113 lines
2.7 KiB
TypeScript
113 lines
2.7 KiB
TypeScript
// scripts/replace-lucide-imports.ts
|
|
|
|
const lucidePath = '@lucide/svelte';
|
|
|
|
function parseSpec(spec: string) {
|
|
const parts = spec.split(/\s+as\s+/).map((s) => s.trim());
|
|
const name = parts[0];
|
|
const alias = parts[1] ?? name;
|
|
return { alias, name };
|
|
}
|
|
|
|
async function processFile(path: string) {
|
|
const file = Bun.file(path);
|
|
const text = await file.text();
|
|
|
|
const regex = /import\s*\{[^}]+\}\s*from\s*["']@lucide\/svelte["'];?/g;
|
|
|
|
const matches = [...text.matchAll(regex)];
|
|
if (matches.length === 0) return;
|
|
|
|
let updatedText = text;
|
|
|
|
for (const match of matches) {
|
|
const block = match[0];
|
|
|
|
const inside = block.match(/\{([^}]+)\}/);
|
|
if (!inside) continue;
|
|
|
|
const items = inside[1]
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
const resolved: string[] = [];
|
|
|
|
for (const item of items) {
|
|
const { name } = parseSpec(item);
|
|
const path = await resolveIconPath(name);
|
|
resolved.push(path);
|
|
}
|
|
|
|
const replacement = transformImportBlock(block, resolved);
|
|
|
|
updatedText = updatedText.replace(block, replacement);
|
|
}
|
|
|
|
if (updatedText !== text) {
|
|
console.log(`Updated ${path}`);
|
|
await Bun.write(path, updatedText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safe import builder with fallback prompt
|
|
*/
|
|
async function resolveIconPath(name: string): Promise<string> {
|
|
let cleanName = name;
|
|
if (cleanName.endsWith('Icon')) {
|
|
cleanName = cleanName.slice(0, -4);
|
|
}
|
|
const kebab = toKebab(cleanName);
|
|
const candidate = `${lucidePath}/icons/${kebab}`;
|
|
|
|
// Bun runtime check: verify physical file exists in dist/icons
|
|
const file = Bun.file(`node_modules/${lucidePath}/dist/icons/${kebab}.js`);
|
|
|
|
if (await file.exists()) return candidate;
|
|
|
|
// fallback interactive prompt
|
|
const input = await prompt(
|
|
`Icon not found: "${name}" → "${kebab}". Enter correct kebab name (or press enter to skip): `
|
|
);
|
|
|
|
if (!input) return candidate; // fallback anyway
|
|
|
|
return `${lucidePath}/icons/${input}`;
|
|
}
|
|
|
|
function toKebab(input: string): string {
|
|
return input
|
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
.replace(/([a-zA-Z])([0-9])/g, '$1-$2') // handle digit transitions like Trash2 -> trash-2
|
|
.toLowerCase();
|
|
}
|
|
|
|
function transformImportBlock(block: string, resolved: string[]): string {
|
|
const inside = block.match(/\{([^}]+)\}/);
|
|
if (!inside) return block;
|
|
|
|
const items = inside[1]
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
const imports: string[] = [];
|
|
|
|
for (const item of items) {
|
|
const { alias } = parseSpec(item);
|
|
const iconPath = resolved.shift();
|
|
|
|
imports.push(`import ${alias} from "${iconPath}";`);
|
|
}
|
|
|
|
return imports.join('\n');
|
|
}
|
|
|
|
const glob = new Bun.Glob('src/**/*.{ts,tsx,js,jsx,svelte}');
|
|
|
|
for await (const file of glob.scan('.')) {
|
|
await processFile(file);
|
|
}
|