babel-plugin-lucide-transform.js
1const fs = require("fs");
2const path = require("path");
3
4const wordSeparators =
5 /[\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]+/;
6const capital_plus_lower = /[A-ZÀ-Ý\u00C0-\u00D6\u00D9-\u00DD][a-zà-ÿ]/g;
7const capitals = /[A-Z0-9À-Ý\u00C0-\u00D6\u00D9-\u00DD]+/g;
8
9function kebabCase(str) {
10 let res = str;
11 res = str.replace(
12 capital_plus_lower,
13 (match) => ` ${match[0].toLowerCase() || match[0]}${match[1]}`,
14 );
15 res = res.replace(capitals, (match) => ` ${match.toLowerCase()}`);
16 return res
17 .trim()
18 .split(wordSeparators)
19 .join("-")
20 .replace(/^-/, "")
21 .replace(/-\s*$/, "");
22}
23
24// Build icon name to file mapping from lucide's export file
25let iconToFileMap;
26function getIconMapping(baseDir) {
27 if (iconToFileMap) return iconToFileMap;
28
29 iconToFileMap = {};
30 try {
31 const lucideExportsPath = path.join(
32 baseDir,
33 "node_modules/lucide-react-native/dist/esm/lucide-react-native.js",
34 );
35 const content = fs.readFileSync(lucideExportsPath, "utf8");
36
37 // Match lines like: export { default as User2, default as UserRound } from './icons/user-round.js';
38 const exportRegex =
39 /export\s*\{([^}]+)\}\s*from\s*['"]\\.\/icons\/([^'"]+)\.js['"]/g;
40
41 let match;
42 while ((match = exportRegex.exec(content)) !== null) {
43 const exports = match[1];
44 const filename = match[2];
45
46 // Extract all icon names from the export list
47 const names = exports
48 .split(",")
49 .map((name) => name.trim())
50 .filter((name) => name.startsWith("default as "))
51 .map((name) => name.replace("default as ", "").trim());
52
53 // Map each name to the filename
54 names.forEach((name) => {
55 iconToFileMap[name] = filename;
56 });
57 }
58 } catch (err) {
59 console.warn(
60 "Failed to parse lucide exports, falling back to kebab-case:",
61 err.message,
62 );
63 iconToFileMap = {};
64 }
65
66 return iconToFileMap;
67}
68
69function lucideTransformPlugin({ types: t }) {
70 return {
71 visitor: {
72 ImportDeclaration(path, state) {
73 const source = path.node.source.value;
74 if (source === "lucide-react-native") {
75 const imports = [];
76 const baseDir = state.file.opts.root || process.cwd();
77 const mapping = getIconMapping(baseDir);
78
79 path.node.specifiers.forEach((specifier) => {
80 if (specifier.type === "ImportSpecifier") {
81 const importedName = specifier.imported.name;
82 const localName = specifier.local.name;
83
84 // Use the mapping if available, otherwise fall back to kebab-case
85 const filename = mapping[importedName] || kebabCase(importedName);
86
87 // Create individual default import for each icon
88 imports.push(
89 t.importDeclaration(
90 [t.importDefaultSpecifier(t.identifier(localName))],
91 t.stringLiteral(
92 `lucide-react-native/dist/esm/icons/${filename}`,
93 ),
94 ),
95 );
96 }
97 });
98
99 // Replace the original import with individual imports
100 path.replaceWithMultiple(imports);
101 }
102 },
103 },
104 };
105}
106
107module.exports = lucideTransformPlugin;