Skip to content

feat(icons): add ux:icons:lock command to "mass import" used icons #1660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Icons/config/iconify.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\UX\Icons\Command\ImportIconCommand;
use Symfony\UX\Icons\Command\LockIconsCommand;
use Symfony\UX\Icons\Iconify;
use Symfony\UX\Icons\Registry\IconifyOnDemandRegistry;

Expand All @@ -36,5 +37,13 @@
service('.ux_icons.local_svg_icon_registry'),
])
->tag('console.command')

->set('.ux_icons.command.lock', LockIconsCommand::class)
->args([
service('.ux_icons.iconify'),
service('.ux_icons.local_svg_icon_registry'),
service('.ux_icons.icon_finder'),
])
->tag('console.command')
;
};
20 changes: 18 additions & 2 deletions src/Icons/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ to fetch the icon and always use the *latest version* of the icon. It's possible
that the icon could change or be removed in the future. Additionally, the cache
warming process will take significantly longer if using many *on-demand* icons.

That's why this package provices a command to download the open source icons into
That's why this package provides a command to download the open source icons into
the ``assets/icons/`` directory. You can think of importing an icon as *locking it*
(similar to how ``composer.lock`` *locks* your dependencies):

Expand All @@ -233,6 +233,23 @@ the ``assets/icons/`` directory. You can think of importing an icon as *locking

Imported icons must be committed to your repository.

Locking On-Demand Icons
~~~~~~~~~~~~~~~~~~~~~~~

You can *lock* (import) all the `*on-demand* <Icons On-Demand>`_ icons you're using in your project by
running the following command:

.. code-block:: terminal

$ php bin/console ux:icons:lock

This command only imports icons that do not already exist locally. You can force
the report to overwrite existing icons by using the ``--force`` option:

.. code-block:: terminal

$ php bin/console ux:icons:lock --force

Rendering Icons
---------------

Expand Down Expand Up @@ -369,7 +386,6 @@ Performance
The UX Icons component is designed to be fast. The following are some of
the optimizations made to ensure the best performance possible.


Caching
-------

Expand Down
2 changes: 1 addition & 1 deletion src/Icons/src/Command/ImportIconCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected function configure(): void
->addArgument(
'names',
InputArgument::IS_ARRAY | InputArgument::REQUIRED,
'Icon name from iconify.design (suffix with "@<name>" to rename locally)',
'Icon name from ux.symfony.com/icons (e.g. "mdi:home")',
)
;
}
Expand Down
101 changes: 101 additions & 0 deletions src/Icons/src/Command/LockIconsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Icons\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\UX\Icons\Exception\IconNotFoundException;
use Symfony\UX\Icons\Iconify;
use Symfony\UX\Icons\Registry\LocalSvgIconRegistry;
use Symfony\UX\Icons\Twig\IconFinder;

/**
* @author Kevin Bond <[email protected]>
*
* @internal
*/
#[AsCommand(
name: 'ux:icons:lock',
description: 'Scan project and import icon(s) from iconify.design',
)]
final class LockIconsCommand extends Command
{
public function __construct(
private Iconify $iconify,
private LocalSvgIconRegistry $registry,
private IconFinder $iconFinder,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->addOption(
name: 'force',
mode: InputOption::VALUE_NONE,
description: 'Force re-import of all found icons'
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$force = $input->getOption('force');
$count = 0;

$io->comment('Scanning project for icons...');

foreach ($this->iconFinder->icons() as $icon) {
if (2 !== \count($parts = explode(':', $icon))) {
continue;
}

if (!$force && $this->registry->has($icon)) {
// icon already imported
continue;
}

[$prefix, $name] = $parts;

try {
$svg = $this->iconify->fetchSvg($prefix, $name);
} catch (IconNotFoundException) {
// icon not found on iconify
continue;
}

$this->registry->add(sprintf('%s/%s', $prefix, $name), $svg);

$license = $this->iconify->metadataFor($prefix)['license'];
++$count;

$io->text(sprintf(
" <fg=bright-green;options=bold>✓</> Imported <fg=bright-white;bg=black>%s:</><fg=bright-magenta;bg=black;options>%s</> (License: <href=%s>%s</>). Render with: <comment>{{ ux_icon('%s') }}</comment>",
$prefix,
$name,
$license['url'],
$license['title'],
$icon,
));
}

$io->success(sprintf('Imported %d icons.', $count));

return Command::SUCCESS;
}
}
11 changes: 11 additions & 0 deletions src/Icons/src/Registry/LocalSvgIconRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ public function get(string $name): Icon
return Icon::fromFile($filename);
}

public function has(string $name): bool
{
try {
$this->get($name);

return true;
} catch (IconNotFoundException) {
return false;
}
}

public function add(string $name, string $svg): void
{
$filename = sprintf('%s/%s.svg', $this->iconDir, $name);
Expand Down
86 changes: 86 additions & 0 deletions src/Icons/tests/Integration/Command/LockIconsCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Icons\Tests\Integration\Command;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Filesystem\Filesystem;
use Zenstruck\Console\Test\InteractsWithConsole;

/**
* @author Kevin Bond <[email protected]>
*/
final class LockIconsCommandTest extends KernelTestCase
{
use InteractsWithConsole;

private const ICONS = [
__DIR__.'/../../Fixtures/icons/iconamoon/3d-duotone.svg',
__DIR__.'/../../Fixtures/icons/flag/eu-4x3.svg',
];

/**
* @before
*
* @after
*/
public static function cleanup(): void
{
$fs = new Filesystem();

foreach (self::ICONS as $icon) {
$fs->remove($icon);
}
}

public function testImportFoundIcons(): void
{
foreach (self::ICONS as $icon) {
$this->assertFileDoesNotExist($icon);
}

$this->executeConsoleCommand('ux:icons:lock')
->assertSuccessful()
->assertOutputContains('Scanning project for icons...')
->assertOutputContains('Imported flag:eu-4x3')
->assertOutputContains('Imported iconamoon:3d-duotone')
->assertOutputContains('Imported 2 icons')
;

foreach (self::ICONS as $icon) {
$this->assertFileExists($icon);
}

$this->executeConsoleCommand('ux:icons:lock')
->assertSuccessful()
->assertOutputContains('Imported 0 icons')
;
}

public function testForceImportFoundIcons(): void
{
$this->executeConsoleCommand('ux:icons:lock')
->assertSuccessful()
->assertOutputContains('Scanning project for icons...')
->assertOutputContains('Imported flag:eu-4x3')
->assertOutputContains('Imported iconamoon:3d-duotone')
->assertOutputContains('Imported 2 icons')
;

$this->executeConsoleCommand('ux:icons:lock --force')
->assertSuccessful()
->assertOutputContains('Scanning project for icons...')
->assertOutputContains('Imported flag:eu-4x3')
->assertOutputContains('Imported iconamoon:3d-duotone')
->assertOutputContains('Imported 2 icons')
;
}
}