Repository: symfony/ux (php)

Issue:
[Icons] Add support for icon aliases during icon:lock command
### Context

Currently, the `IconFinder` only scans templates to identify used icons, using the following regex:

```php
$token = '[a-z0-9]+(?:-[a-z0-9]+)*';
$pattern = "#(?:'$token:$token')|(?:\"$token:$token\")#i";
```

This approach means that only icons directly referenced in templates are locked when running the `icon:lock` command.

### Problem

Some icons are defined as aliases in the `ux_icons` configuration file. These aliased icons are not detected by `IconFinder` and therefore are not locked automatically, which can lead to inconsistencies during deployment.

### Request

Would it be possible to add support for scanning icon aliases defined in the `ux_icons` file, in addition to using the regex to search templates? This enhancement would ensure that all icons actually used in the application are properly locked.

Pull request description:
[Icons] Support aliases in LockIconsCommand
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | Fix #2354
| License       | MIT

Alias icons are now all locked, and icon set aliases are supported too.


Patch:
diff --git a/src/Icons/src/Command/LockIconsCommand.php b/src/Icons/src/Command/LockIconsCommand.php
index c3b3d2fa..69fab8e9 100644
--- a/src/Icons/src/Command/LockIconsCommand.php
+++ b/src/Icons/src/Command/LockIconsCommand.php
@@ -37,6 +37,8 @@ final class LockIconsCommand extends Command
         private Iconify $iconify,
         private LocalSvgIconRegistry $registry,
         private IconFinder $iconFinder,
+        private readonly array $iconAliases = [],
+        private readonly array $iconSetAliases = [],
     ) {
         parent::__construct();
     }
@@ -59,19 +61,25 @@ final class LockIconsCommand extends Command
         $count = 0;
 
         $io->comment('Scanning project for icons...');
+        $finderIcons = $this->iconFinder->icons();
 
-        foreach ($this->iconFinder->icons() as $icon) {
+        if ($this->iconAliases) {
+            $io->comment('Adding icons aliases...');
+        }
+
+        foreach ([...array_values($this->iconAliases), ...array_values($finderIcons)] as $icon) {
             if (2 !== \count($parts = explode(':', $icon))) {
                 continue;
             }
 
-            if (!$force && $this->registry->has($icon)) {
+            [$prefix, $name] = $parts;
+            $prefix = $this->iconSetAliases[$prefix] ?? $prefix;
+
+            if (!$force && $this->registry->has($prefix.':'.$name)) {
                 // icon already imported
                 continue;
             }
 
-            [$prefix, $name] = $parts;
-
             if (!$this->iconify->hasIconSet($prefix)) {
                 // not an icon set? example: "og:twitter"
                 if ($io->isVeryVerbose()) {
diff --git a/src/Icons/src/DependencyInjection/UXIconsExtension.php b/src/Icons/src/DependencyInjection/UXIconsExtension.php
index 618749ee..81042277 100644
--- a/src/Icons/src/DependencyInjection/UXIconsExtension.php
+++ b/src/Icons/src/DependencyInjection/UXIconsExtension.php
@@ -178,6 +178,11 @@ final class UXIconsExtension extends ConfigurableExtension implements Configurat
             }
         }
 
+        $container->getDefinition('.ux_icons.command.lock')
+            ->setArgument(3, $mergedConfig['aliases'])
+            ->setArgument(4, $iconSetAliases)
+        ;
+
         if (!$container->getParameter('kernel.debug')) {
             $container->removeDefinition('.ux_icons.command.import');
         }


Test patch:
diff --git a/src/Icons/tests/Fixtures/TestKernel.php b/src/Icons/tests/Fixtures/TestKernel.php
index 6a9d0f0b..2f573439 100644
--- a/src/Icons/tests/Fixtures/TestKernel.php
+++ b/src/Icons/tests/Fixtures/TestKernel.php
@@ -60,6 +60,10 @@ final class TestKernel extends Kernel
 
         $container->extension('ux_icons', [
             'icon_dir' => '%kernel.project_dir%/tests/Fixtures/icons',
+            'aliases' => [
+                'foo' => 'lucide:circle',
+                'bar' => 'lu:circle-off',
+            ],
             'icon_sets' => [
                 'fla' => [
                     'path' => '%kernel.project_dir%/tests/Fixtures/images/flags',
diff --git a/src/Icons/tests/Integration/Command/LockIconsCommandTest.php b/src/Icons/tests/Integration/Command/LockIconsCommandTest.php
index f6b2e11f..abcfc45f 100644
--- a/src/Icons/tests/Integration/Command/LockIconsCommandTest.php
+++ b/src/Icons/tests/Integration/Command/LockIconsCommandTest.php
@@ -25,6 +25,8 @@ final class LockIconsCommandTest extends KernelTestCase
     private const ICONS = [
         __DIR__.'/../../Fixtures/icons/iconamoon/3d-duotone.svg',
         __DIR__.'/../../Fixtures/icons/flag/eu-4x3.svg',
+        __DIR__.'/../../Fixtures/icons/lucide/circle.svg',
+        __DIR__.'/../../Fixtures/icons/lucide/circle-off.svg',
     ];
 
     /**
@@ -50,9 +52,11 @@ final class LockIconsCommandTest extends KernelTestCase
         $this->executeConsoleCommand('ux:icons:lock')
             ->assertSuccessful()
             ->assertOutputContains('Scanning project for icons...')
+            ->assertOutputContains('Imported lucide:circle')
+            ->assertOutputContains('Imported lucide:circle-off')
             ->assertOutputContains('Imported flag:eu-4x3')
             ->assertOutputContains('Imported iconamoon:3d-duotone')
-            ->assertOutputContains('Imported 2 icons')
+            ->assertOutputContains('Imported 4 icons')
         ;
 
         foreach (self::ICONS as $icon) {
@@ -70,17 +74,21 @@ final class LockIconsCommandTest extends KernelTestCase
         $this->executeConsoleCommand('ux:icons:lock')
             ->assertSuccessful()
             ->assertOutputContains('Scanning project for icons...')
+            ->assertOutputContains('Imported lucide:circle')
+            ->assertOutputContains('Imported lucide:circle-off')
             ->assertOutputContains('Imported flag:eu-4x3')
             ->assertOutputContains('Imported iconamoon:3d-duotone')
-            ->assertOutputContains('Imported 2 icons')
+            ->assertOutputContains('Imported 4 icons')
         ;
 
         $this->executeConsoleCommand('ux:icons:lock --force')
             ->assertSuccessful()
             ->assertOutputContains('Scanning project for icons...')
+            ->assertOutputContains('Imported lucide:circle')
+            ->assertOutputContains('Imported lucide:circle-off')
             ->assertOutputContains('Imported flag:eu-4x3')
             ->assertOutputContains('Imported iconamoon:3d-duotone')
-            ->assertOutputContains('Imported 2 icons')
+            ->assertOutputContains('Imported 4 icons')
         ;
     }
 }