rdicons 0.3.0 → 0.4.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 +14 -0
  2. package/dist/cli.js +232 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,6 +18,7 @@ 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
21
22
  rdicons sets list the 19 libraries with their licenses
22
23
  ```
23
24
 
@@ -30,6 +31,19 @@ rdicons sets list the 19 libraries with their licenses
30
31
  | `--stroke` | the icon's own | stroke width (outline icons) |
31
32
  | `--padding` | `0` | extra space around the icon |
32
33
  | `--background` | none | solid background |
34
+ | `--animated [mode]` | off | rd icons only: `hover` (default), `parent`, `play`, `loop` |
35
+ | `--speed` | `1` | animation speed (with `--animated`) |
36
+
37
+ ## Switch icon library: `migrate`
38
+
39
+ Moving a project from one icon library to another usually means editing every import by hand. `migrate` does it for you:
40
+
41
+ ```bash
42
+ npx rdicons migrate src --to tabler # preview: nothing is changed
43
+ npx rdicons migrate src --to rd --write # apply it (commit your work first)
44
+ ```
45
+
46
+ 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.
33
47
  | `--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
48
  | `--speed <n>` | `1` | animation speed, `2` is twice as fast (needs `--animated`) |
35
49
 
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 resolve2 } from "path";
6
6
 
7
7
  // ../core/dist/index.js
8
8
  var STROKE_WIDTH_RE = /stroke-width="[^"]*"/g;
@@ -124,8 +124,214 @@ export function Example() {
124
124
  ];
125
125
  }
126
126
 
127
+ // src/migrate.ts
128
+ import { readdir, readFile, stat, writeFile } from "fs/promises";
129
+ import { join, relative, resolve } from "path";
130
+ 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();
131
+ var stripSuffix = (s, suffix) => s.length > suffix.length && s.endsWith(suffix) ? s.slice(0, -suffix.length) : s;
132
+ var stripPrefix = (s, prefix) => s.length > prefix.length && s.startsWith(prefix) ? s.slice(prefix.length) : s;
133
+ var lucide = (framework) => ({ set: "lucide", framework, slug: (n) => kebab(stripSuffix(stripPrefix(n, "Lucide"), "Icon")) });
134
+ var REACT_ICONS = {
135
+ fi: { set: "feather" },
136
+ lu: { set: "lucide" },
137
+ tb: { set: "tabler" },
138
+ pi: { set: "ph" },
139
+ ri: { set: "ri" },
140
+ bs: { set: "bi" },
141
+ go: { set: "octicon" },
142
+ rx: { set: "radix-icons" },
143
+ hi2: { set: "heroicons" },
144
+ md: { set: "material-symbols" }
145
+ };
146
+ function sourceFor(spec) {
147
+ if (spec === "lucide-react") return lucide("react");
148
+ if (spec === "lucide-vue-next") return lucide("vue");
149
+ 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)' };
150
+ if (spec === "@tabler/icons-vue") return { set: "tabler", framework: "vue", slug: (n) => kebab(stripPrefix(n, "Icon")) };
151
+ 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' };
152
+ const hero = spec.match(/^@heroicons\/(react|vue)\/(?:\d\d\/)?(outline|solid)$/);
153
+ if (hero) return { set: "heroicons", framework: hero[1], slug: (n) => kebab(stripSuffix(n, "Icon")), style: hero[2] === "solid" ? "filled" : "outline" };
154
+ const ri = spec.match(/^react-icons\/([a-z0-9]+)$/);
155
+ if (ri && REACT_ICONS[ri[1]]) {
156
+ const pack = ri[1];
157
+ const prefix = pack === "hi2" ? "Hi" : pack[0].toUpperCase() + pack.slice(1);
158
+ return {
159
+ set: REACT_ICONS[pack].set,
160
+ framework: "react",
161
+ slug: (n) => {
162
+ if (!n.startsWith(prefix)) return null;
163
+ let s = kebab(n.slice(prefix.length));
164
+ if (pack === "hi2") s = s.replace(/^(outline|solid|mini)-/, "");
165
+ if (pack === "md") s = s.replace(/-outline$/, "");
166
+ return s;
167
+ },
168
+ style: void 0
169
+ };
170
+ }
171
+ return null;
172
+ }
173
+ var ALIASES = [
174
+ ["home", "house"],
175
+ ["trash", "trash-2", "bin", "delete"],
176
+ ["settings", "cog", "gear", "setting"],
177
+ ["search", "magnifier", "magnifying-glass"],
178
+ ["mail", "envelope", "email"],
179
+ ["user", "person", "account"],
180
+ ["edit", "pencil", "pen"],
181
+ ["x", "close", "times", "cross"],
182
+ ["check", "tick"],
183
+ ["plus", "add"],
184
+ ["minus", "remove"],
185
+ ["bell", "notification"],
186
+ ["star", "favorite"],
187
+ ["share", "share-2"],
188
+ ["lock", "padlock"],
189
+ ["calendar", "calendar-days"],
190
+ ["camera", "photo-camera"],
191
+ ["image", "picture", "photo"],
192
+ ["eye", "visibility"],
193
+ ["link", "chain"],
194
+ ["folder", "directory"],
195
+ ["file", "document"],
196
+ ["message", "chat", "comment"],
197
+ ["phone", "telephone", "call"],
198
+ ["heart", "favourite"],
199
+ ["arrow-left", "arrow-small-left", "left-arrow"],
200
+ ["arrow-right", "arrow-small-right", "right-arrow"],
201
+ ["chevron-down", "angle-down", "caret-down"],
202
+ ["chevron-up", "angle-up", "caret-up"],
203
+ ["chevron-left", "angle-left", "caret-left"],
204
+ ["chevron-right", "angle-right", "caret-right"]
205
+ ];
206
+ var STYLE_SUFFIX = /-(fill|filled|solid|line|bold|thin|light|duotone|regular|outline|sharp)$/;
207
+ var styleOf = (slug) => /-(fill|filled|solid)$/.test(slug) ? "filled" : "outline";
208
+ function findIcon(slug, target, forceStyle) {
209
+ const style = forceStyle ?? styleOf(slug);
210
+ const base = slug.replace(STYLE_SUFFIX, "");
211
+ const group = ALIASES.find((g) => g.includes(base));
212
+ const bases = [base, ...group ? group.filter((x) => x !== base) : []];
213
+ const variants = style === "filled" ? ["-fill", "-filled", "-solid", "-fill-24"] : ["", "-line", "-regular", "-outline", "-24-regular"];
214
+ const candidates = [slug, ...bases.flatMap((b) => variants.map((v) => b + v)), ...bases];
215
+ for (const c of candidates) {
216
+ const comp = target.get(c);
217
+ if (comp !== void 0) return { name: c, comp };
218
+ }
219
+ return null;
220
+ }
221
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".svelte-kit", "coverage", "out", ".turbo"]);
222
+ async function* walk(path, exts) {
223
+ const s = await stat(path);
224
+ if (s.isFile()) {
225
+ yield path;
226
+ return;
227
+ }
228
+ for (const e of await readdir(path, { withFileTypes: true })) {
229
+ if (e.isDirectory()) {
230
+ if (!SKIP_DIRS.has(e.name)) yield* walk(join(path, e.name), exts);
231
+ } else if (exts.has(e.name.slice(e.name.lastIndexOf(".") + 1).toLowerCase())) yield join(path, e.name);
232
+ }
233
+ }
234
+ var IMPORT = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\3([ \t]*;?)([ \t]*\r?\n)?/g;
235
+ async function migrate(paths, opts, deps) {
236
+ const targetNames = new Map(await deps.names(opts.to));
237
+ const exts = new Set(opts.exts);
238
+ const files = [];
239
+ const frameworks = /* @__PURE__ */ new Set();
240
+ let changed = 0;
241
+ const seen = /* @__PURE__ */ new Set();
242
+ for (const p of paths) {
243
+ for await (const file of walk(resolve(p), exts)) {
244
+ if (seen.has(file)) continue;
245
+ seen.add(file);
246
+ const text = await readFile(file, "utf8");
247
+ if (!/import\s+(type\s+)?\{/.test(text)) continue;
248
+ const report = { file, migrated: [], unmatched: [], warnings: [] };
249
+ const jsxFixes = [];
250
+ const merged = /* @__PURE__ */ new Map();
251
+ const out = text.replace(IMPORT, (whole, typeKw, list, quote, spec, semi, nl) => {
252
+ const eol = nl ?? "";
253
+ const source = sourceFor(spec);
254
+ if (!source) return whole;
255
+ const kept = [];
256
+ const moved = [];
257
+ const hit = [];
258
+ for (const raw of list.split(",").map((x) => x.trim()).filter(Boolean)) {
259
+ const isType = /^type\s+/.test(raw);
260
+ const clean = raw.replace(/^type\s+/, "");
261
+ const [exported, local] = clean.split(/\s+as\s+/).map((x) => x.trim());
262
+ const slug = source.slug(exported);
263
+ const found = slug ? findIcon(slug, targetNames, source.style) : null;
264
+ if (!found || isType) {
265
+ kept.push(raw);
266
+ if (!isType) report.unmatched.push(`${exported} (${spec})`);
267
+ continue;
268
+ }
269
+ const localName = local ?? exported;
270
+ moved.push(found.comp === localName ? found.comp : `${found.comp} as ${localName}`);
271
+ hit.push(`${exported} -> ${found.name}`);
272
+ if (spec === "@tabler/icons-react") jsxFixes.push(localName);
273
+ }
274
+ if (!moved.length) return whole;
275
+ frameworks.add(source.framework);
276
+ report.migrated.push(...hit.map((h) => {
277
+ const [from, to] = h.split(" -> ");
278
+ return { from: `${source.set}:${from}`, to: `${opts.to}:${to}` };
279
+ }));
280
+ if (source.warn) report.warnings.push(source.warn);
281
+ const pkg = `@rdicons/${source.framework}/${opts.to}`;
282
+ const q = quote;
283
+ const key = `${typeKw ?? ""}|${pkg}`;
284
+ const keptLine = kept.length ? `import ${typeKw ?? ""}{ ${kept.join(", ")} } from ${q}${spec}${q}${semi}${eol || "\n"}` : "";
285
+ const existing = merged.get(key);
286
+ if (existing) {
287
+ existing.specs.push(...moved);
288
+ return keptLine;
289
+ }
290
+ merged.set(key, { specs: [...moved], line: (specs) => `import ${typeKw ?? ""}{ ${specs.join(", ")} } from ${q}${pkg}${q}${semi}${eol}` });
291
+ return `${keptLine}\0RD:${key}\0`;
292
+ }).replace(/\u0000RD:([^\u0000]*)\u0000/g, (_, key) => {
293
+ const m = merged.get(key);
294
+ return m.line([...new Set(m.specs)]);
295
+ });
296
+ if (out === text) {
297
+ if (report.unmatched.length) files.push(report);
298
+ continue;
299
+ }
300
+ let final = out;
301
+ for (const name of new Set(jsxFixes)) final = final.replace(new RegExp(`(<${name}\\b[^>]*?)\\sstroke=`, "g"), "$1 strokeWidth=");
302
+ changed++;
303
+ files.push(report);
304
+ if (opts.write) await writeFile(file, final, "utf8");
305
+ }
306
+ }
307
+ return {
308
+ files,
309
+ changed,
310
+ written: opts.write,
311
+ installHint: [...frameworks].map((f) => `npm install @rdicons/${f}`)
312
+ };
313
+ }
314
+ function formatReport(r, root) {
315
+ const lines = [];
316
+ let moved = 0;
317
+ const left = [];
318
+ for (const f of r.files) {
319
+ moved += f.migrated.length;
320
+ 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"}`);
321
+ for (const u of f.unmatched) left.push(` ${relative(root, f.file) || f.file}: ${u}`);
322
+ }
323
+ lines.push("");
324
+ lines.push(`${moved} icon${moved === 1 ? "" : "s"} in ${r.changed} file${r.changed === 1 ? "" : "s"} ${r.written ? "rewritten" : "can be rewritten"}.`);
325
+ if (left.length) lines.push("", `No match in the target library (left on the old import, pick a replacement by hand):`, ...left);
326
+ const warnings = [...new Set(r.files.flatMap((f) => f.warnings))];
327
+ if (warnings.length) lines.push("", "Check:", ...warnings.map((w) => " - " + w));
328
+ if (r.installHint.length && r.changed) lines.push("", `Then install the new package: ${r.installHint.join(" and ")}`);
329
+ 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).");
330
+ return lines.join("\n");
331
+ }
332
+
127
333
  // src/cli.ts
128
- var VERSION = "0.3.0";
334
+ var VERSION = "0.4.0";
129
335
  var BASE = (process.env.RDICONS_URL || "https://rdicons.vimalbhatt.com").replace(/\/$/, "");
130
336
  var HELP = `rdicons ${VERSION}: add open-source icons to your project
131
337
 
@@ -133,6 +339,7 @@ Usage
133
339
  rdicons add <set:name> [more...] [options] download icons into a folder
134
340
  rdicons search <query> [--set lucide] find icons by name
135
341
  rdicons info <set:name> show the code to use one icon (every install option)
342
+ rdicons migrate <path...> --to <library> switch a project's icon imports to another library (preview first)
136
343
  rdicons sets list the 19 icon libraries
137
344
 
138
345
  Options for "add"
@@ -146,7 +353,14 @@ Options for "add"
146
353
  --animated [mode] add the icon's animation (rd icons): hover (default), parent, play or loop
147
354
  --speed <number> animation speed, 2 is twice as fast (default 1; needs --animated)
148
355
 
356
+ Options for "migrate"
357
+ --to <library> the library to move to, e.g. lucide, tabler, ph, rd (see "rdicons sets")
358
+ --write apply the changes (without it you only get a preview)
359
+ --ext js,ts,tsx,... which files to look at (default js,jsx,ts,tsx,mjs,cjs,vue)
360
+
149
361
  Examples
362
+ npx rdicons migrate src --to tabler preview: lucide-react imports -> @rdicons/react/tabler
363
+ npx rdicons migrate src --to rd --write apply it
150
364
  npx rdicons add lucide:house
151
365
  npx rdicons add tabler:bell ph:heart --format tsx --out src/icons --color "#4f46e5"
152
366
  npx rdicons add rd:bell --animated loop --speed 1.5 --format tsx
@@ -201,6 +415,7 @@ function parseArgs(argv) {
201
415
  if (a.startsWith("--")) {
202
416
  const [k, inline] = a.slice(2).split("=");
203
417
  if (inline !== void 0) flags[k] = inline;
418
+ else if (k === "write") flags[k] = "true";
204
419
  else if (k === "animated") flags[k] = MODES.includes(argv[i + 1]) ? argv[++i] : "true";
205
420
  else if (argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--")) flags[k] = argv[++i];
206
421
  else flags[k] = "true";
@@ -262,7 +477,7 @@ async function cmdAdd(positional, flags) {
262
477
  if (!positional.length) fail("Which icon? For example: rdicons add lucide:house");
263
478
  const format = flags.format ?? "svg";
264
479
  if (!["svg", "jsx", "tsx"].includes(format)) fail("--format must be svg, jsx or tsx");
265
- const out = resolve(flags.out ?? "icons");
480
+ const out = resolve2(flags.out ?? "icons");
266
481
  const mode = flags.animated === void 0 ? null : flags.animated === "true" ? "hover" : flags.animated;
267
482
  if (mode && !MODES.includes(mode)) fail(`--animated must be one of ${MODES.join(", ")} (or just --animated for hover)`);
268
483
  const speed = flags.speed === void 0 ? 1 : Number(flags.speed);
@@ -289,11 +504,21 @@ async function cmdAdd(positional, flags) {
289
504
  const file = format === "svg" ? `${it.s}-${it.n}.svg` : `${comp}.${format}`;
290
505
  const jsx = format === "svg" ? "" : svgToJsx(svg, comp);
291
506
  const text = format === "svg" ? svg + "\n" : (anim ? jsxifyAnimated(jsx, format === "tsx") : jsx) + "\n";
292
- await writeFile(join(out, file), text, "utf8");
293
- console.log(`\u2713 ${it.s}:${it.n} -> ${join(flags.out ?? "icons", file)}`);
507
+ await writeFile2(join2(out, file), text, "utf8");
508
+ console.log(`\u2713 ${it.s}:${it.n} -> ${join2(flags.out ?? "icons", file)}`);
294
509
  }
295
510
  console.log('\nTip: run "rdicons sets" to see each library license (some need attribution).');
296
511
  }
512
+ async function cmdMigrate(positional, flags) {
513
+ if (!positional.length) fail("Which folder or file? For example: rdicons migrate src --to tabler");
514
+ const to = flags.to;
515
+ if (!to || to === "true") fail('Which library do you want to move to? For example: --to tabler (run "rdicons sets" for the list)');
516
+ const sets = await listSets();
517
+ if (!sets.some((s) => s.prefix === to)) fail(`Unknown library "${to}". Choose one of: ${sets.map((s) => s.prefix).join(", ")}`);
518
+ const exts = (flags.ext && flags.ext !== "true" ? flags.ext : "js,jsx,ts,tsx,mjs,cjs,vue").split(",").map((x) => x.trim().replace(/^\./, "").toLowerCase());
519
+ const result = await migrate(positional, { to, write: flags.write === "true", exts }, { names: (set) => getJson(`/api/names?set=${encodeURIComponent(set)}`) });
520
+ console.log(formatReport(result, process.cwd()));
521
+ }
297
522
  async function main() {
298
523
  const [cmd, ...rest] = process.argv.slice(2);
299
524
  const { flags, positional } = parseArgs(rest);
@@ -303,6 +528,7 @@ async function main() {
303
528
  if (cmd === "search") return cmdSearch(positional, flags);
304
529
  if (cmd === "info") return cmdInfo(positional);
305
530
  if (cmd === "add") return cmdAdd(positional, flags);
531
+ if (cmd === "migrate") return cmdMigrate(positional, flags);
306
532
  fail(`Unknown command "${cmd}". Run rdicons --help`);
307
533
  }
308
534
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdicons",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",