Skip to content

Commit ebbf193

Browse files
committed
[Toolkit] Improve InstallComponentCommand by asking/guessing which Kit to use, remove ux_toolkit.kit parameter, remove DependencyInjection configuration
1 parent cf9496e commit ebbf193

File tree

9 files changed

+87
-84
lines changed

9 files changed

+87
-84
lines changed

src/Toolkit/config/services.php

-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
->set('.ux_toolkit.command.install', InstallComponentCommand::class)
3737
->args([
38-
param('ux_toolkit.kit'),
3938
service('.ux_toolkit.registry.registry_factory'),
4039
service('filesystem'),
4140
])
@@ -55,7 +54,6 @@
5554
->args([
5655
service('.ux_toolkit.kit.kit_factory'),
5756
service('filesystem'),
58-
param('kernel.project_dir'),
5957
])
6058

6159
->set('.ux_toolkit.registry.github', GitHubRegistry::class)

src/Toolkit/doc/index.rst

+12-30
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ It uses the same approach than the popular `Shadcn UI`_,
1717
and a similar approach than `Tailwind Plus`_.
1818

1919
After installing the UX Toolkit, you can start pulling the components you need
20-
from the `UX Components page`_, and use them in your project.
21-
They become your own components, and you can customize them as you want.
20+
from `UX Toolkit Kits`_, and use them in your project.
21+
They become **your own components**, and **you can customize them as you want**.
2222

2323
Additionally, some `Twig components`_ use ``html_cva`` and ``tailwind_merge``,
2424
you can either remove them from your project or install ``twig/html-extra``
@@ -40,27 +40,16 @@ Install the UX Toolkit using Composer and Symfony Flex:
4040
# If you want to keep `html_cva` and `tailwind_merge` in your Twig components:
4141
$ composer require twig/extra-bundle twig/html-extra:^3.12.0 tales-from-a-dev/twig-tailwind-extra
4242
43-
Configuration
44-
-------------
45-
46-
Configuration is done in your ``config/packages/ux_toolkit.yaml`` file:
47-
48-
.. code-block:: yaml
49-
50-
# config/packages/ux_toolkit.yaml
51-
ux_toolkit:
52-
kit: 'shadcn'
53-
5443
Usage
5544
-----
5645

5746
You may find a list of components in the `UX Components page`_, with the installation instructions for each of them.
5847

59-
For example, if you want to install the `Button` component, you will find the following instruction:
48+
For example, if you want to install a `Button` component, you will find the following instruction:
6049

6150
.. code-block:: terminal
6251
63-
$ php bin/console ux:toolkit:install-component Button
52+
$ php bin/console ux:toolkit:install-component Button --kit=<kitName>
6453
6554
It will create the ``templates/components/Button.html.twig`` file, and you will be able to use the `Button` component like this:
6655

@@ -121,24 +110,17 @@ A kit is composed of:
121110
- A ``templates/components`` directory, that contains the Twig components,
122111
- A ``docs/components`` directory, optional, that contains the documentation for each "root" Twig component.
123112

124-
Use your kit in a Symfony application
125-
-------------------------------------
113+
Using your kit
114+
~~~~~~~~~~~~~~
126115

127-
You can globally configure the kit to use in your application by setting the ``ux_toolkit.kit`` configuration:
128-
129-
.. code-block:: yaml
130-
131-
# config/packages/ux_toolkit.yaml
132-
ux_toolkit:
133-
kit: 'github.com/my-username/my-ux-kits'
134-
# or for a specific version
135-
kit: 'github.com/my-username/my-ux-kits:1.0.0'
136-
137-
If you do not want to globally configure the kit, you can pass the ``--kit`` option to the ``ux:toolkit:install-component`` command:
116+
Once your kit is published on GitHub, you can use it by specifying the ``--kit`` option when installing a component:
138117

139118
.. code-block:: terminal
140119
141-
$ php bin/console ux:toolkit:install-component Button --kit=github.com/my-username/my-ux-kits
120+
$ php bin/console ux:toolkit:install-component Button --kit=github.com/my-username/my-ux-toolkit-kit
121+
122+
# or for a specific version
123+
$ php bin/console ux:toolkit:install-component Button --kit=github.com/my-username/my-ux-toolkit-kit:1.0.0
142124
143125
Backward Compatibility promise
144126
------------------------------
@@ -153,6 +135,6 @@ We may break them in patch or minor release, but you won't get impacted unless y
153135

154136
.. _`the Symfony UX initiative`: https://ux.symfony.com/
155137
.. _`Twig components`: https://symfony.com/bundles/ux-twig-component/current/index.html
156-
.. _`UX Components page`: https://ux.symfony.com/components
138+
.. _`UX Toolkit Kits`: https://ux.symfony.com/toolkit#kits
157139
.. _`Shadcn UI`: https://ui.shadcn.com/
158140
.. _`Tailwind Plus`: https://tailwindcss.com/plus

src/Toolkit/src/Command/InstallComponentCommand.php

+44-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\UX\Toolkit\File\File;
2525
use Symfony\UX\Toolkit\Installer\Installer;
2626
use Symfony\UX\Toolkit\Kit\Kit;
27+
use Symfony\UX\Toolkit\Registry\LocalRegistry;
2728
use Symfony\UX\Toolkit\Registry\RegistryFactory;
2829

2930
/**
@@ -42,7 +43,6 @@ class InstallComponentCommand extends Command
4243
private bool $isInteractive;
4344

4445
public function __construct(
45-
private readonly string $kitName,
4646
private readonly RegistryFactory $registryFactory,
4747
private readonly Filesystem $filesystem,
4848
) {
@@ -53,6 +53,7 @@ protected function configure(): void
5353
{
5454
$this
5555
->addArgument('component', InputArgument::OPTIONAL, 'The component name (Ex: Button)')
56+
->addOption('kit', 'k', InputOption::VALUE_OPTIONAL, 'The kit name (Ex: shadcn, or github.com/user/my-ux-toolkit-kit)')
5657
->addOption(
5758
'destination',
5859
'd',
@@ -61,7 +62,6 @@ protected function configure(): void
6162
Path::join('templates', 'components')
6263
)
6364
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the component installation, even if the component already exists')
64-
->addOption('kit', 'k', InputOption::VALUE_OPTIONAL, 'Override the kit name', $this->kitName)
6565
->setHelp(
6666
<<<EOF
6767
The <info>%command.name%</info> command will install a new UX Component in your project.
@@ -92,10 +92,48 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9292
$io = new SymfonyStyle($input, $output);
9393

9494
$kitName = $input->getOption('kit');
95-
$registry = $this->registryFactory->getForKit($kitName);
96-
$kit = $registry->getKit($kitName);
95+
$componentName = $input->getArgument('component');
96+
97+
// If the kit name is not explicitly provided, we need to suggest one
98+
if (null === $kitName) {
99+
/** @var list<Kit> $availableKits */
100+
$availableKits = [];
101+
$availableKitNames = LocalRegistry::getAvailableKitsName();
102+
foreach ($availableKitNames as $availableKitName) {
103+
$kit = $this->registryFactory->getForKit($availableKitName)->getKit($availableKitName);
104+
105+
if (null === $componentName) {
106+
$availableKits[] = $kit;
107+
} elseif (null !== $kit->getComponent($componentName)) {
108+
$availableKits[] = $kit;
109+
}
110+
}
111+
// If more than one kit is available, we ask the user which one to use
112+
if (($availableKitsCount = \count($availableKits)) > 1) {
113+
$kitName = $io->choice(null === $componentName ? 'Which kit do you want to use?' : \sprintf('The component "%s" exists in multiple kits. Which one do you want to use?', $componentName), array_map(fn (Kit $kit) => $kit->name, $availableKits));
114+
115+
foreach ($availableKits as $availableKit) {
116+
if ($availableKit->name === $kitName) {
117+
$kit = $availableKit;
118+
break;
119+
}
120+
}
121+
} elseif (1 === $availableKitsCount) {
122+
$kit = $availableKits[0];
123+
} else {
124+
$io->error(null === $componentName
125+
? 'It seems that no local kits are available and it should not happens. Please open an issue on https://github.com/symfony/ux to report this.'
126+
: sprintf("The component \"%s\" does not exist in any local kits.\n\nYou can try to run one of the following commands to interactively install components:\n%s\n\nOr you can try one of the community kits https://github.com/search?q=topic:ux-toolkit&type=repositories", $componentName, implode("\n", array_map(fn (string $availableKitName) => sprintf('$ bin/console %s --kit %s', $this->getName(), $availableKitName), $availableKitNames)))
127+
);
128+
129+
return Command::FAILURE;
130+
}
131+
} else {
132+
$registry = $this->registryFactory->getForKit($kitName);
133+
$kit = $registry->getKit($kitName);
134+
}
97135

98-
if (null === $componentName = $input->getArgument('component')) {
136+
if (null === $componentName) {
99137
// Ask for the component name if not provided
100138
$componentName = $io->choice('Which component do you want to install?', array_map(fn (Component $component) => $component->name, $this->getAvailableComponents($kit)));
101139
$component = $kit->getComponent($componentName);
@@ -124,7 +162,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
124162
}
125163
}
126164

127-
$io->writeln(\sprintf('Installing component <info>%s</> from the <info>%s</> kit...', $component->name, $kitName));
165+
$io->writeln(\sprintf('Installing component <info>%s</> from the <info>%s</> kit...', $component->name, $kit->name));
128166

129167
$installer = new Installer($this->filesystem, fn (string $question) => $this->io->confirm($question, $input->isInteractive()));
130168
$installationReport = $installer->installComponent($kit, $component, $destinationPath = $input->getOption('destination'), $input->getOption('force'));

src/Toolkit/src/Registry/LocalRegistry.php

+24-12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Filesystem\Filesystem;
1515
use Symfony\Component\Filesystem\Path;
16+
use Symfony\Component\Finder\Finder;
1617
use Symfony\UX\Toolkit\Kit\Kit;
1718
use Symfony\UX\Toolkit\Kit\KitFactory;
1819

@@ -24,6 +25,8 @@
2425
*/
2526
final class LocalRegistry implements Registry
2627
{
28+
private static string $kitsDir = __DIR__.\DIRECTORY_SEPARATOR.'..'.\DIRECTORY_SEPARATOR.'..'.\DIRECTORY_SEPARATOR.'kits';
29+
2730
public static function supports(string $kitName): bool
2831
{
2932
return 1 === preg_match('/^[a-zA-Z0-9_-]+$/', $kitName);
@@ -32,25 +35,34 @@ public static function supports(string $kitName): bool
3235
public function __construct(
3336
private readonly KitFactory $kitFactory,
3437
private readonly Filesystem $filesystem,
35-
private readonly string $projectDir,
3638
) {
3739
}
3840

3941
public function getKit(string $kitName): Kit
4042
{
41-
$possibleKitDirs = [
42-
// Local kit
43-
Path::join($this->projectDir, 'kits', $kitName),
44-
// From vendor
45-
Path::join($this->projectDir, 'vendor', 'symfony', 'ux-toolkit', 'kits', $kitName),
46-
];
47-
48-
foreach ($possibleKitDirs as $kitDir) {
49-
if ($this->filesystem->exists($kitDir)) {
50-
return $this->kitFactory->createKitFromAbsolutePath($kitDir);
51-
}
43+
$kitDir = Path::join(self::$kitsDir, $kitName);
44+
if ($this->filesystem->exists($kitDir)) {
45+
return $this->kitFactory->createKitFromAbsolutePath($kitDir);
5246
}
5347

5448
throw new \RuntimeException(\sprintf('Unable to find the kit "%s" in the following directories: "%s"', $kitName, implode('", "', $possibleKitDirs)));
5549
}
50+
51+
/**
52+
* @return array<string>
53+
*/
54+
public static function getAvailableKitsName(): array
55+
{
56+
$availableKitsName = [];
57+
$finder = (new Finder())->directories()->in(self::$kitsDir)->depth(0);
58+
59+
foreach ($finder as $directory) {
60+
$kitName = $directory->getRelativePathname();
61+
if (self::supports($kitName)) {
62+
$availableKitsName[] = $kitName;
63+
}
64+
}
65+
66+
return $availableKitsName;
67+
}
5668
}

src/Toolkit/src/UXToolkitBundle.php

-23
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\UX\Toolkit;
1313

14-
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
1514
use Symfony\Component\DependencyInjection\ContainerBuilder;
1615
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1716
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
@@ -24,30 +23,8 @@ class UXToolkitBundle extends AbstractBundle
2423
{
2524
protected string $extensionAlias = 'ux_toolkit';
2625

27-
public function configure(DefinitionConfigurator $definition): void
28-
{
29-
$rootNode = $definition->rootNode();
30-
$rootNode
31-
->children()
32-
->scalarNode('kit')
33-
->info('The kit to use, it can be from the official UX Toolkit repository, or an external GitHub repository')
34-
->defaultValue('shadcn')
35-
->example([
36-
'shadcn',
37-
'github.com/user/repository@my-kit',
38-
'github.com/user/repository@my-kit:main',
39-
'https://github.com/user/repository@my-kit',
40-
])
41-
->end()
42-
->end();
43-
}
44-
4526
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
4627
{
47-
$container->parameters()
48-
->set('ux_toolkit.kit', $config['kit'])
49-
;
50-
5128
$container->import('../config/services.php');
5229
}
5330
}

src/Toolkit/tests/Command/InstallComponentCommandTest.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function testShouldAbleToInstallComponentTableAndItsDependencies(): void
5353
$testCommand = $this->consoleCommand('ux:toolkit:install-component Table --destination='.$this->tmpDir)
5454
->execute()
5555
->assertSuccessful()
56-
->assertOutputContains('Installing component Table from the shadcn kit...')
56+
->assertOutputContains('Installing component Table from the Shadcn UI kit...')
5757
->assertOutputContains('[OK] The component has been installed.')
5858
;
5959

@@ -65,16 +65,16 @@ public function testShouldAbleToInstallComponentTableAndItsDependencies(): void
6565
}
6666
}
6767

68-
public function testShouldFailAndSuggestAlternativeComponents(): void
68+
public function testShouldFailAndSuggestAlternativeComponentsWhenKitIsExplicit(): void
6969
{
7070
$destination = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid();
7171
mkdir($destination);
7272

7373
$this->bootKernel();
74-
$this->consoleCommand('ux:toolkit:install-component Table: --destination='.$destination)
74+
$this->consoleCommand('ux:toolkit:install-component Table: --kit=shadcn --destination='.$destination)
7575
->execute()
7676
->assertFaulty()
77-
->assertOutputContains('[WARNING] The component "Table:" does not exist.')
77+
->assertOutputContains('[WARNING] The component "Table:" does not exist')
7878
->assertOutputContains('Possible alternatives: ')
7979
->assertOutputContains('"Table:Body"')
8080
->assertOutputContains('"Table:Caption"')
@@ -95,7 +95,7 @@ public function testShouldFailWhenComponentDoesNotExist(): void
9595
$this->consoleCommand('ux:toolkit:install-component Unknown --destination='.$destination)
9696
->execute()
9797
->assertFaulty()
98-
->assertOutputContains('The component "Unknown" does not exist.');
98+
->assertOutputContains('The component "Unknown" does not exist');
9999
}
100100

101101
public function testShouldWarnWhenComponentFileAlreadyExistsInNonInteractiveMode(): void

src/Toolkit/tests/UXToolkitBundleTest.php

-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,5 @@ public function testBundleBuildsSuccessfully(): void
2222
$container = self::$kernel->getContainer();
2323

2424
$this->assertInstanceOf(UXToolkitBundle::class, $container->get('kernel')->getBundles()['UXToolkitBundle']);
25-
$this->assertEquals('shadcn', $container->getParameter('ux_toolkit.kit'));
2625
}
2726
}

ux.symfony.com/config/packages/ux_toolkit.yaml

-2
This file was deleted.

ux.symfony.com/src/Twig/Components/Toolkit/ComponentDoc.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,11 @@ private function formatContent(string $markdownContent): string
5555
private function insertInstallation(AbstractString $markdownContent): AbstractString
5656
{
5757
$installationCode = SourceCleaner::processTerminalLines(<<<SHELL
58-
symfony console ux:toolkit:install-component {$this->component->name}
59-
# or if you already use another kit
60-
symfony console ux:toolkit:install-component {$this->component->name} --kit {$this->kitId->value}
58+
bin/console ux:toolkit:install-component {$this->component->name} --kit {$this->kitId->value}
6159
SHELL
6260
);
6361

62+
// TODO: Provide tabs showing automatic and manual installation
6463
return $markdownContent->replace(
6564
'<!-- Placeholder: Installation -->',
6665
<<<HTML

0 commit comments

Comments
 (0)