Skip to content

Commit 4a41bc3

Browse files
committed
iterate
1 parent 3ea6c28 commit 4a41bc3

File tree

13 files changed

+99
-250
lines changed

13 files changed

+99
-250
lines changed

src/Toolkit/doc/index.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ For example, if you want to install a `Button` component, you will find the foll
4949

5050
.. code-block:: terminal
5151
52-
$ php bin/console ux:toolkit:install-component Button --kit=<kitName>
52+
$ php bin/console ux:install Button --kit=<kitName>
5353
5454
It will create the ``templates/components/Button.html.twig`` file, and you will be able to use the `Button` component like this:
5555

@@ -118,10 +118,10 @@ Once your kit is published on GitHub, you can use it by specifying the ``--kit``
118118

119119
.. code-block:: terminal
120120
121-
$ php bin/console ux:toolkit:install-component Button --kit=github.com/my-username/my-ux-toolkit-kit
121+
$ php bin/console ux:install Button --kit=github.com/my-username/my-ux-toolkit-kit
122122
123123
# or for a specific version
124-
$ php bin/console ux:toolkit:install-component Button --kit=github.com/my-username/my-ux-toolkit-kit:1.0.0
124+
$ php bin/console ux:install Button --kit=github.com/my-username/my-ux-toolkit-kit:1.0.0
125125
126126
Backward Compatibility promise
127127
------------------------------

src/Toolkit/src/Command/InstallComponentCommand.php

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
use Symfony\Component\Console\Style\SymfonyStyle;
2121
use Symfony\Component\Filesystem\Filesystem;
2222
use Symfony\Component\Filesystem\Path;
23-
use Symfony\UX\Toolkit\Asset\Component;
2423
use Symfony\UX\Toolkit\File\File;
2524
use Symfony\UX\Toolkit\Installer\Installer;
2625
use Symfony\UX\Toolkit\Kit\Kit;
26+
use Symfony\UX\Toolkit\Recipe\Recipe;
27+
use Symfony\UX\Toolkit\Recipe\RecipeType;
2728
use Symfony\UX\Toolkit\Registry\LocalRegistry;
2829
use Symfony\UX\Toolkit\Registry\RegistryFactory;
2930

@@ -34,13 +35,12 @@
3435
* @internal
3536
*/
3637
#[AsCommand(
37-
name: 'ux:toolkit:install-component',
38-
description: 'Install a new UX Component (e.g. Alert) in your project',
38+
name: 'ux:install-component',
39+
description: 'Install a new UX Toolkit recipe in your project',
3940
)]
4041
class InstallComponentCommand extends Command
4142
{
4243
private SymfonyStyle $io;
43-
private bool $isInteractive;
4444

4545
public function __construct(
4646
private readonly RegistryFactory $registryFactory,
@@ -53,29 +53,26 @@ 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)')
56+
->addOption('kit', 'k', InputOption::VALUE_OPTIONAL, 'The kit name (Ex: "shadcn", or "github.com/user/my-ux-toolkit-kit")')
5757
->addOption(
5858
'destination',
5959
'd',
6060
InputOption::VALUE_OPTIONAL,
6161
'The destination directory',
62-
Path::join('templates', 'components')
62+
getcwd(),
6363
)
64-
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the component installation, even if the component already exists')
64+
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the component installation, even if the files already exists')
6565
->setHelp(
6666
<<<EOF
6767
The <info>%command.name%</info> command will install a new UX Component in your project.
6868
69-
To install a component from your current kit, use:
69+
To install a component, use:
7070
7171
<info>php %command.full_name% Button</info>
7272
73-
To install a component from an official UX Toolkit kit, use the <info>--kit</info> option:
73+
To install a component from a specific Kit (either official or external), use the <info>--kit</info> option:
7474
7575
<info>php %command.full_name% Button --kit=shadcn</info>
76-
77-
To install a component from an external GitHub kit, use the <info>--kit</info> option:
78-
7976
<info>php %command.full_name% Button --kit=https://github.com/user/my-kit</info>
8077
<info>php %command.full_name% Button --kit=https://github.com/user/my-kit:branch</info>
8178
EOF
@@ -104,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
104101

105102
if (null === $componentName) {
106103
$availableKits[] = $kit;
107-
} elseif (null !== $kit->getComponent($componentName)) {
104+
} elseif (null !== $kit->getRecipe(name: $componentName, type: RecipeType::Component)) {
108105
$availableKits[] = $kit;
109106
}
110107
}
@@ -113,7 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
113110
$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));
114111

115112
foreach ($availableKits as $availableKit) {
116-
if ($availableKit->name === $kitName) {
113+
if ($availableKit->manifest->name === $kitName) {
117114
$kit = $availableKit;
118115
break;
119116
}
@@ -123,7 +120,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
123120
} else {
124121
$io->error(null === $componentName
125122
? '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)))
123+
: \sprintf("The component \"%s\" does not exist in any official 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)))
127124
);
128125

129126
return Command::FAILURE;
@@ -135,24 +132,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int
135132

136133
if (null === $componentName) {
137134
// Ask for the component name if not provided
138-
$componentName = $io->choice('Which component do you want to install?', array_map(fn (Component $component) => $component->name, $this->getAvailableComponents($kit)));
139-
$component = $kit->getComponent($componentName);
140-
} elseif (null === $component = $kit->getComponent($componentName)) {
135+
$componentName = $io->choice('Which component do you want to install?', array_map(fn (Recipe $recipe) => $recipe->manifest->name, $this->getAvailableRecipes($kit)));
136+
$recipe = $kit->getRecipe(name: $componentName, type: RecipeType::Component);
137+
} elseif (null === $recipe = $kit->getRecipe($componentName, type: RecipeType::Component)) {
141138
// Suggest alternatives if component does not exist
142139
$message = \sprintf('The component "%s" does not exist.', $componentName);
143140

144-
$alternativeComponents = $this->getAlternativeComponents($kit, $componentName);
145-
$alternativeComponentsCount = \count($alternativeComponents);
141+
$alternativeRecipes = $this->getAlternativeRecipes($kit, $componentName);
142+
$alternativeComponentsCount = \count($alternativeRecipes);
146143

147144
if (1 === $alternativeComponentsCount && $input->isInteractive()) {
148145
$io->warning($message);
149-
if ($io->confirm(\sprintf('Do you want to install the component "%s" instead?', $alternativeComponents[0]->name))) {
150-
$component = $alternativeComponents[0];
146+
if ($io->confirm(\sprintf('Do you want to install the component "%s" instead?', $alternativeRecipes[0]->manifest->name))) {
147+
$recipe = $alternativeRecipes[0];
151148
} else {
152149
return Command::FAILURE;
153150
}
154151
} elseif ($alternativeComponentsCount > 0) {
155-
$io->warning(\sprintf('%s'."\n".'Possible alternatives: "%s"', $message, implode('", "', array_map(fn (Component $c) => $c->name, $alternativeComponents))));
152+
$io->warning(\sprintf('%s'."\n".'Possible alternatives: "%s"', $message, implode('", "', array_map(fn (Recipe $c) => $c->manifest->name, $alternativeRecipes))));
156153

157154
return Command::FAILURE;
158155
} else {
@@ -162,10 +159,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
162159
}
163160
}
164161

165-
$io->writeln(\sprintf('Installing component <info>%s</> from the <info>%s</> kit...', $component->name, $kit->name));
162+
$io->writeln(\sprintf('Installing component <info>%s</> from the <info>%s</> kit...', $recipe->manifest->name, $kit->manifest->name));
166163

167164
$installer = new Installer($this->filesystem, fn (string $question) => $this->io->confirm($question, $input->isInteractive()));
168-
$installationReport = $installer->installComponent($kit, $component, $destinationPath = $input->getOption('destination'), $input->getOption('force'));
165+
$installationReport = $installer->installRecipe($kit, $recipe, $destinationPath = $input->getOption('destination'), $input->getOption('force'));
169166

170167
if ([] === $installationReport->newFiles) {
171168
$this->io->warning('The component has not been installed.');
@@ -186,34 +183,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int
186183
}
187184

188185
/**
189-
* @return list<Component>
186+
* @return list<Recipe>
190187
*/
191-
private function getAvailableComponents(Kit $kit): array
188+
private function getAvailableRecipes(Kit $kit): array
192189
{
193190
$availableComponents = [];
194191

195-
foreach ($kit->getComponents() as $component) {
196-
if (str_contains($component->name, ':')) {
197-
continue;
198-
}
199-
200-
$availableComponents[] = $component;
192+
foreach ($kit->getRecipes(type: RecipeType::Component) as $recipe) {
193+
$availableComponents[] = $recipe;
201194
}
202195

203196
return $availableComponents;
204197
}
205198

206199
/**
207-
* @return list<Component>
200+
* @return list<Recipe>
208201
*/
209-
private function getAlternativeComponents(Kit $kit, string $componentName): array
202+
private function getAlternativeRecipes(Kit $kit, string $componentName): array
210203
{
211204
$alternative = [];
212205

213-
foreach ($kit->getComponents() as $component) {
214-
$lev = levenshtein($componentName, $component->name, 2, 5, 10);
215-
if ($lev <= 8 || str_contains($component->name, $componentName)) {
216-
$alternative[] = $component;
206+
foreach ($kit->getRecipes(type: RecipeType::Component) as $recipe) {
207+
$lev = levenshtein($componentName, $recipe->manifest->name, 2, 5, 10);
208+
if ($lev <= 8 || str_contains($recipe->manifest->name, $componentName)) {
209+
$alternative[] = $recipe;
217210
}
218211
}
219212

src/Toolkit/src/Installer/Installer.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\UX\Toolkit\Asset\Component;
1919
use Symfony\UX\Toolkit\File\File;
2020
use Symfony\UX\Toolkit\Kit\Kit;
21+
use Symfony\UX\Toolkit\Recipe\Recipe;
2122

2223
final class Installer
2324
{
@@ -33,9 +34,9 @@ public function __construct(
3334
$this->poolResolver = new PoolResolver();
3435
}
3536

36-
public function installComponent(Kit $kit, Component $component, string $destinationPath, bool $force): InstallationReport
37+
public function installRecipe(Kit $kit, Recipe $recipe, string $destinationPath, bool $force): InstallationReport
3738
{
38-
$pool = $this->poolResolver->resolveForComponent($kit, $component);
39+
$pool = $this->poolResolver->resolveForRecipe($kit, $recipe);
3940
$output = $this->handlePool($pool, $kit, $destinationPath, $force);
4041

4142
return $output;
@@ -62,16 +63,18 @@ private function handlePool(Pool $pool, Kit $kit, string $destinationPath, bool
6263
*/
6364
private function installFile(Kit $kit, File $file, string $destinationPath, bool $force): bool
6465
{
65-
$componentPath = Path::join($kit->absolutePath, $file->relativePathName);
66-
$componentDestinationPath = Path::join($destinationPath, $file->toRelativePathName);
66+
$sourceAbsolutePathName = Path::join($kit->absolutePath, $file->sourceRelativePathName);
67+
$destinationAbsolutePathName = Path::join($destinationPath, $file->destinationRelativePathName);
6768

68-
if ($this->filesystem->exists($componentDestinationPath) && !$force) {
69-
if (!($this->askConfirmation)(\sprintf('File "%s" already exists. Do you want to overwrite it?', $componentDestinationPath))) {
69+
dump(source: $sourceAbsolutePathName, destination: $destinationAbsolutePathName, force: $force);
70+
71+
if ($this->filesystem->exists($destinationAbsolutePathName) && !$force) {
72+
if (!($this->askConfirmation)(\sprintf('File "%s" already exists. Do you want to overwrite it?', $destinationAbsolutePathName))) {
7073
return false;
7174
}
7275
}
7376

74-
$this->filesystem->copy($componentPath, $componentDestinationPath, $force);
77+
$this->filesystem->copy($sourceAbsolutePathName, $destinationAbsolutePathName, $force);
7578

7679
return true;
7780
}

src/Toolkit/src/Installer/Pool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class Pool
3737

3838
public function addFile(File $file): void
3939
{
40-
$this->files[$file->toRelativePathName] ??= $file;
40+
$this->files[$file->destinationRelativePathName] ??= $file;
4141
}
4242

4343
/**

src/Toolkit/src/Installer/PoolResolver.php

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,49 +13,44 @@
1313

1414
namespace Symfony\UX\Toolkit\Installer;
1515

16-
use Symfony\UX\Toolkit\Asset\Component;
17-
use Symfony\UX\Toolkit\Dependency\ComponentDependency;
1816
use Symfony\UX\Toolkit\Dependency\PhpPackageDependency;
19-
use Symfony\UX\Toolkit\Dependency\StimulusControllerDependency;
17+
use Symfony\UX\Toolkit\Dependency\RecipeDependency;
2018
use Symfony\UX\Toolkit\Kit\Kit;
19+
use Symfony\UX\Toolkit\Recipe\Recipe;
2120

2221
final class PoolResolver
2322
{
24-
public function resolveForComponent(Kit $kit, Component $component): Pool
23+
public function resolveForRecipe(Kit $kit, Recipe $recipe): Pool
2524
{
2625
$pool = new Pool();
2726

2827
// Process the component and its dependencies
29-
$componentsStack = [$component];
30-
$visitedComponents = new \SplObjectStorage();
28+
$recipesStack = [$recipe];
29+
$visitedRecipes = new \SplObjectStorage();
3130

32-
while (!empty($componentsStack)) {
33-
$currentComponent = array_pop($componentsStack);
31+
while (!empty($recipesStack)) {
32+
$currentComponent = array_pop($recipesStack);
3433

3534
// Skip circular references
36-
if ($visitedComponents->contains($currentComponent)) {
35+
if ($visitedRecipes->contains($currentComponent)) {
3736
continue;
3837
}
3938

40-
$visitedComponents->attach($currentComponent);
39+
$visitedRecipes->attach($currentComponent);
4140

42-
foreach ($currentComponent->files as $file) {
41+
foreach ($recipe->getFiles() as $file) {
4342
$pool->addFile($file);
4443
}
4544

46-
foreach ($currentComponent->getDependencies() as $dependency) {
47-
if ($dependency instanceof ComponentDependency) {
48-
$componentsStack[] = $kit->getComponent($dependency->name);
49-
} elseif ($dependency instanceof PhpPackageDependency) {
45+
foreach ($recipe->manifest->dependencies as $dependency) {
46+
if ($dependency instanceof PhpPackageDependency) {
5047
$pool->addPhpPackageDependency($dependency);
51-
} elseif ($dependency instanceof StimulusControllerDependency) {
52-
if (null === $stimulusController = $kit->getStimulusController($dependency->name)) {
53-
throw new \RuntimeException(\sprintf('Stimulus controller "%s" not found.', $dependency->name));
48+
} elseif($dependency instanceof RecipeDependency) {
49+
if (null === $recipeDependency = $kit->getRecipe($dependency->name)) {
50+
throw new \LogicException(sprintf('The recipe "%s" has a dependency on unregistered recipe "%s".', $recipe->manifest->name, $dependency->name));
5451
}
5552

56-
foreach ($stimulusController->files as $file) {
57-
$pool->addFile($file);
58-
}
53+
$recipesStack[] = $recipeDependency;
5954
} else {
6055
throw new \RuntimeException(\sprintf('Unknown dependency type: "%s"', $dependency::class));
6156
}

src/Toolkit/src/Kit/Kit.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,23 @@ public function addRecipe(Recipe $recipe): void
5656
/**
5757
* @return array<Recipe>
5858
*/
59-
public function getRecipes(?RecipeType $ofType = null): array
59+
public function getRecipes(?RecipeType $type = null): array
6060
{
61-
if (null !== $ofType) {
62-
$this->recipes = array_filter($this->recipes, fn (Recipe $recipe) => $recipe->manifest->type === $ofType);
61+
if (null !== $type) {
62+
$this->recipes = array_filter($this->recipes, fn (Recipe $recipe) => $recipe->manifest->type === $type);
6363
}
6464

6565
return $this->recipes;
6666
}
67+
68+
public function getRecipe(string $name, ?RecipeType $type = null): ?Recipe
69+
{
70+
foreach ($this->recipes as $recipe) {
71+
if ($recipe->manifest->name === $name && ($type === null || $recipe->manifest->type === $type)) {
72+
return $recipe;
73+
}
74+
}
75+
76+
return null;
77+
}
6778
}

src/Toolkit/src/Kit/KitContextRunner.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private function contextualizeServicesForKit(Kit $kit): callable
5757
// Configure Twig
5858
$initialTwigLoader = $this->twig->getLoader();
5959
$loaders = [];
60-
foreach ($kit->getRecipes(ofType: RecipeType::Component) as $recipe) {
60+
foreach ($kit->getRecipes(type: RecipeType::Component) as $recipe) {
6161
// TODO: We assume that components are in the "templates/components" directory
6262
$loaders[] = new FilesystemLoader(Path::join($recipe->absolutePath, 'templates/components'));
6363
}
@@ -92,7 +92,7 @@ public function __construct(private readonly Kit $kit)
9292

9393
public function findAnonymousComponentTemplate(string $name): ?string
9494
{
95-
foreach ($this->kit->getRecipes(ofType: RecipeType::Component) as $recipe) {
95+
foreach ($this->kit->getRecipes(type: RecipeType::Component) as $recipe) {
9696
// TODO: We assume that the component can be found in the recipe with the same name
9797
if ($recipe->manifest->name === $name) {
9898
foreach ($recipe->getFiles() as $file) {

0 commit comments

Comments
 (0)