diff --git a/src/Toolkit/kits/shadcn/Alert/manifest.json b/src/Toolkit/kits/shadcn/Alert/manifest.json index 979175eac1d..7aed0768409 100644 --- a/src/Toolkit/kits/shadcn/Alert/manifest.json +++ b/src/Toolkit/kits/shadcn/Alert/manifest.json @@ -9,15 +9,16 @@ "dependencies": [ { "type": "php", - "package": "twig/extra-bundle" + "name": "twig/extra-bundle" }, { "type": "php", - "package": "twig/html-extra:^3.12.0" + "name": "twig/html-extra", + "version": "^3.12.0" }, { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/AspectRatio/manifest.json b/src/Toolkit/kits/shadcn/AspectRatio/manifest.json index 8666063aa52..9ad0bca70b3 100644 --- a/src/Toolkit/kits/shadcn/AspectRatio/manifest.json +++ b/src/Toolkit/kits/shadcn/AspectRatio/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "twig/extra-bundle" + "name": "twig/extra-bundle" } ] } diff --git a/src/Toolkit/kits/shadcn/Avatar/manifest.json b/src/Toolkit/kits/shadcn/Avatar/manifest.json index 7b3276b3b13..6154e377859 100644 --- a/src/Toolkit/kits/shadcn/Avatar/manifest.json +++ b/src/Toolkit/kits/shadcn/Avatar/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Badge/manifest.json b/src/Toolkit/kits/shadcn/Badge/manifest.json index 2ab4631bbed..1791976b1ab 100644 --- a/src/Toolkit/kits/shadcn/Badge/manifest.json +++ b/src/Toolkit/kits/shadcn/Badge/manifest.json @@ -9,15 +9,16 @@ "dependencies": [ { "type": "php", - "package": "twig/extra-bundle" + "name": "twig/extra-bundle" }, { "type": "php", - "package": "twig/html-extra:^3.12.0" + "name": "twig/html-extra", + "version": "^3.12.0" }, { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Breadcrumb/manifest.json b/src/Toolkit/kits/shadcn/Breadcrumb/manifest.json index efe56fb0609..9c8a5ecae18 100644 --- a/src/Toolkit/kits/shadcn/Breadcrumb/manifest.json +++ b/src/Toolkit/kits/shadcn/Breadcrumb/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Button/manifest.json b/src/Toolkit/kits/shadcn/Button/manifest.json index 5f01925efb3..6b97f83a9a4 100644 --- a/src/Toolkit/kits/shadcn/Button/manifest.json +++ b/src/Toolkit/kits/shadcn/Button/manifest.json @@ -9,15 +9,16 @@ "dependencies": [ { "type": "php", - "package": "twig/extra-bundle" + "name": "twig/extra-bundle" }, { "type": "php", - "package": "twig/html-extra:^3.12.0" + "name": "twig/html-extra", + "version": "^3.12.0" }, { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Card/manifest.json b/src/Toolkit/kits/shadcn/Card/manifest.json index 6de056e54f2..ce24f3ced1f 100644 --- a/src/Toolkit/kits/shadcn/Card/manifest.json +++ b/src/Toolkit/kits/shadcn/Card/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Checkbox/manifest.json b/src/Toolkit/kits/shadcn/Checkbox/manifest.json index 97f8c3443da..3e36e4cc3a5 100644 --- a/src/Toolkit/kits/shadcn/Checkbox/manifest.json +++ b/src/Toolkit/kits/shadcn/Checkbox/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Input/manifest.json b/src/Toolkit/kits/shadcn/Input/manifest.json index 5c778e58dcf..37710cff064 100644 --- a/src/Toolkit/kits/shadcn/Input/manifest.json +++ b/src/Toolkit/kits/shadcn/Input/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Label/manifest.json b/src/Toolkit/kits/shadcn/Label/manifest.json index a52d0baeea1..83947d1c9e5 100644 --- a/src/Toolkit/kits/shadcn/Label/manifest.json +++ b/src/Toolkit/kits/shadcn/Label/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Pagination/manifest.json b/src/Toolkit/kits/shadcn/Pagination/manifest.json index 5a456dc3b93..98daba20f12 100644 --- a/src/Toolkit/kits/shadcn/Pagination/manifest.json +++ b/src/Toolkit/kits/shadcn/Pagination/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Progress/manifest.json b/src/Toolkit/kits/shadcn/Progress/manifest.json index eff5fcf874c..07615b3d75f 100644 --- a/src/Toolkit/kits/shadcn/Progress/manifest.json +++ b/src/Toolkit/kits/shadcn/Progress/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Select/manifest.json b/src/Toolkit/kits/shadcn/Select/manifest.json index 66e2a60ff79..273f2f91190 100644 --- a/src/Toolkit/kits/shadcn/Select/manifest.json +++ b/src/Toolkit/kits/shadcn/Select/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Separator/manifest.json b/src/Toolkit/kits/shadcn/Separator/manifest.json index 6dd248fd1b1..e308fff9df9 100644 --- a/src/Toolkit/kits/shadcn/Separator/manifest.json +++ b/src/Toolkit/kits/shadcn/Separator/manifest.json @@ -9,15 +9,16 @@ "dependencies": [ { "type": "php", - "package": "twig/extra-bundle" + "name": "twig/extra-bundle" }, { "type": "php", - "package": "twig/html-extra:^3.12.0" + "name": "twig/html-extra", + "version": "^3.12.0" }, { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Skeleton/manifest.json b/src/Toolkit/kits/shadcn/Skeleton/manifest.json index b1ac224d6f4..e873bbe0ecd 100644 --- a/src/Toolkit/kits/shadcn/Skeleton/manifest.json +++ b/src/Toolkit/kits/shadcn/Skeleton/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Switch/manifest.json b/src/Toolkit/kits/shadcn/Switch/manifest.json index 83fb86f9f2f..8d9649a2ce4 100644 --- a/src/Toolkit/kits/shadcn/Switch/manifest.json +++ b/src/Toolkit/kits/shadcn/Switch/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Table/manifest.json b/src/Toolkit/kits/shadcn/Table/manifest.json index a4d3c86b851..a7d9772a0ce 100644 --- a/src/Toolkit/kits/shadcn/Table/manifest.json +++ b/src/Toolkit/kits/shadcn/Table/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/kits/shadcn/Textarea/manifest.json b/src/Toolkit/kits/shadcn/Textarea/manifest.json index 37cf9414e23..3649bb01fce 100644 --- a/src/Toolkit/kits/shadcn/Textarea/manifest.json +++ b/src/Toolkit/kits/shadcn/Textarea/manifest.json @@ -9,7 +9,7 @@ "dependencies": [ { "type": "php", - "package": "tales-from-a-dev/twig-tailwind-extra" + "name": "tales-from-a-dev/twig-tailwind-extra" } ] } diff --git a/src/Toolkit/schema-kit-recipe-v1.json b/src/Toolkit/schema-kit-recipe-v1.json index 039d546f4cb..81891bfa6c7 100644 --- a/src/Toolkit/schema-kit-recipe-v1.json +++ b/src/Toolkit/schema-kit-recipe-v1.json @@ -34,15 +34,53 @@ { "description": "A dependency on a PHP package", "type": "object", - "required": ["type", "package"], + "required": ["type", "name"], "properties": { "type": { "type": "string", "enum": ["php"] }, + "name": { + "type": "string", + "description": "Package name and optional version constraint (e.g., 'vendor/package')" + }, + "version": { + "type": "string", + "description": "Optional version constraint (e.g., '^1.0')" + } + } + }, + { + "description": "A dependency on an NPM package", + "type": "object", + "required": ["type", "name"], + "properties": { + "type": { + "type": "string", + "enum": ["npm"] + }, + "name": { + "type": "string", + "description": "Package name and optional version constraint (e.g., 'vendor/package')" + }, + "version": { + "type": "string", + "description": "Optional version constraint (e.g., '^1.0')" + } + } + }, + { + "description": "A dependency on an Importmap package", + "type": "object", + "required": ["type", "package"], + "properties": { + "type": { + "type": "string", + "enum": ["importmap"] + }, "package": { "type": "string", - "description": "Package name and optional version constraint (e.g., 'vendor/package:^1.0')" + "description": "Importmap package name (e.g., 'lodash', 'bootstrap/dist/css/bootstrap.min.css')" } } }, diff --git a/src/Toolkit/src/Assert.php b/src/Toolkit/src/Assert.php index 7bcebdcc6f0..d72d02494c8 100644 --- a/src/Toolkit/src/Assert.php +++ b/src/Toolkit/src/Assert.php @@ -55,4 +55,19 @@ public static function phpPackageName(string $name): void throw new \InvalidArgumentException(\sprintf('Invalid PHP package name "%s".', $name)); } } + + /** + * Assert that the NPM package name is valid (ex: "react", "@hotwired/stimulus", etc.). + * + * @param non-empty-string $name + * + * @throws \InvalidArgumentException if the NPM package name is invalid + */ + public static function npmPackageName(string $name): void + { + // Taken from https://github.com/dword-design/package-name-regex/blob/master/src/index.ts + if (1 !== preg_match('/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/', $name)) { + throw new \InvalidArgumentException(\sprintf('Invalid NPM package name "%s".', $name)); + } + } } diff --git a/src/Toolkit/src/Command/DebugKitCommand.php b/src/Toolkit/src/Command/DebugKitCommand.php index 9d472271943..b71e18f0f16 100644 --- a/src/Toolkit/src/Command/DebugKitCommand.php +++ b/src/Toolkit/src/Command/DebugKitCommand.php @@ -85,8 +85,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'Dependencies', ]) ->addRow([ - implode("\n", iterator_to_array($recipe->getFiles())), - implode("\n", $recipe->manifest->dependencies), + implode("\n", iterator_to_array($recipe->getFiles())) ?: 'N/A', + implode("\n", $recipe->manifest->dependencies) ?: 'N/A', ]) ->setColumnWidth(1, 80) ->setColumnMaxWidth(1, 80) diff --git a/src/Toolkit/src/Command/InstallCommand.php b/src/Toolkit/src/Command/InstallCommand.php index d9e4f65af58..3400348a524 100644 --- a/src/Toolkit/src/Command/InstallCommand.php +++ b/src/Toolkit/src/Command/InstallCommand.php @@ -171,11 +171,40 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->io->success('The recipe has been installed.'); - $this->io->writeln('The following file(s) have been added to your project:'); + + $this->io->section('Installed files'); $this->io->listing(array_map(fn (File $file) => Path::join($destinationPath, $file->sourceRelativePathName), $installationReport->newFiles)); + if ([] !== $installationReport->suggestedPhpPackages || [] !== $installationReport->suggestedNpmPackages || [] !== $installationReport->suggestedImportmapPackages) { + $this->io->section('Next steps'); + } + + $stepIndex = 0; if ([] !== $installationReport->suggestedPhpPackages) { - $this->io->writeln(\sprintf('Run composer require %s to install the required PHP dependencies.', implode(' ', $installationReport->suggestedPhpPackages))); + $this->io->writeln(++$stepIndex.'. Install suggested PHP package(s) with the command:'); + $this->io->newLine(); + $this->io->writeln(\sprintf(' $ composer require %s', implode(' ', $installationReport->suggestedPhpPackages))); + $this->io->newLine(); + } + + if ([] !== $installationReport->suggestedNpmPackages && [] !== $installationReport->suggestedImportmapPackages) { + $this->io->writeln(++$stepIndex.'. Install suggested front-end packages with one of the following commands:'); + $this->io->newLine(); + $this->io->writeln(' # with npm/pnpm/yarn'); + $this->io->writeln(\sprintf(' $ npm install --save %s', implode(' ', $installationReport->suggestedNpmPackages))); + $this->io->newLine(); + $this->io->writeln(' # or with Importmap'); + $this->io->writeln(\sprintf(' $ php bin/console importmap:install %s', implode(' ', $installationReport->suggestedImportmapPackages))); + $this->io->newLine(); + } elseif ([] !== $installationReport->suggestedNpmPackages) { + $this->io->writeln(++$stepIndex.'. Install suggested front-end package(s) with the command:'); + $this->io->newLine(); + $this->io->writeln(\sprintf(' $ npm install --save %s', implode(' ', $installationReport->suggestedNpmPackages))); + $this->io->newLine(); + } elseif ([] !== $installationReport->suggestedImportmapPackages) { + $this->io->writeln(++$stepIndex.'. Install suggested front-end package(s) with the command:'); + $this->io->newLine(); + $this->io->writeln(\sprintf(' $ php bin/console importmap:install %s', implode(' ', $installationReport->suggestedImportmapPackages))); $this->io->newLine(); } diff --git a/src/Toolkit/src/Dependency/ImportmapPackageDependency.php b/src/Toolkit/src/Dependency/ImportmapPackageDependency.php new file mode 100644 index 00000000000..7627d4ea27b --- /dev/null +++ b/src/Toolkit/src/Dependency/ImportmapPackageDependency.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Toolkit\Dependency; + +/** + * Represents a dependency on an Importmap package. + * + * @internal + * + * @author Hugo Alliaume + */ +final class ImportmapPackageDependency implements DependencyInterface +{ + /** + * @param non-empty-string $package + */ + public function __construct( + public readonly string $package, + ) { + } + + public function isEquivalentTo(DependencyInterface $dependency): bool + { + if (!$dependency instanceof self) { + return false; + } + + return $this->package === $dependency->package; + } + + public function toDebug(): string + { + return \sprintf('Importmap package "%s"', $this->package); + } + + public function __toString(): string + { + return $this->package; + } +} diff --git a/src/Toolkit/src/Dependency/NpmPackageDependency.php b/src/Toolkit/src/Dependency/NpmPackageDependency.php new file mode 100644 index 00000000000..3b789a6c626 --- /dev/null +++ b/src/Toolkit/src/Dependency/NpmPackageDependency.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Toolkit\Dependency; + +use Symfony\UX\Toolkit\Assert; + +/** + * Represents a dependency on an NPM package. + * + * @internal + * + * @author Hugo Alliaume + */ +final class NpmPackageDependency implements DependencyInterface +{ + /** + * @param non-empty-string $name + */ + public function __construct( + public readonly string $name, + public readonly ?Version $constraintVersion = null, + ) { + Assert::npmPackageName($name); + } + + public function isEquivalentTo(DependencyInterface $dependency): bool + { + if (!$dependency instanceof self) { + return false; + } + + return $this->name === $dependency->name; + } + + public function isHigherThan(self $dependency): bool + { + if (null === $this->constraintVersion || null === $dependency->constraintVersion) { + return false; + } + + return $this->constraintVersion->isHigherThan($dependency->constraintVersion); + } + + public function toDebug(): string + { + return \sprintf('NPM package "%s"', $this->__toString()); + } + + public function __toString(): string + { + return $this->name.(null !== $this->constraintVersion ? ':'.$this->constraintVersion : ''); + } +} diff --git a/src/Toolkit/src/Installer/InstallationReport.php b/src/Toolkit/src/Installer/InstallationReport.php index 2844367e28e..6ecca718f90 100644 --- a/src/Toolkit/src/Installer/InstallationReport.php +++ b/src/Toolkit/src/Installer/InstallationReport.php @@ -13,6 +13,8 @@ namespace Symfony\UX\Toolkit\Installer; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; use Symfony\UX\Toolkit\Dependency\PhpPackageDependency; use Symfony\UX\Toolkit\File; @@ -26,12 +28,16 @@ final class InstallationReport { /** - * @param array $newFiles - * @param array $suggestedPhpPackages + * @param array $newFiles + * @param array $suggestedPhpPackages + * @param array $suggestedNpmPackages + * @param array $suggestedImportmapPackages */ public function __construct( public readonly array $newFiles, public readonly array $suggestedPhpPackages, + public readonly array $suggestedNpmPackages, + public readonly array $suggestedImportmapPackages, ) { } } diff --git a/src/Toolkit/src/Installer/Installer.php b/src/Toolkit/src/Installer/Installer.php index 4c0a6e05096..8747623e21a 100644 --- a/src/Toolkit/src/Installer/Installer.php +++ b/src/Toolkit/src/Installer/Installer.php @@ -58,7 +58,7 @@ private function handlePool(Pool $pool, Kit $kit, string $destinationPath, bool } } - return new InstallationReport(newFiles: $installedFiles, suggestedPhpPackages: $pool->getPhpPackageDependencies()); + return new InstallationReport(newFiles: $installedFiles, suggestedPhpPackages: $pool->getPhpPackageDependencies(), suggestedNpmPackages: $pool->getNpmPackageDependencies(), suggestedImportmapPackages: $pool->getImportmapPackageDependencies()); } private function copyFile(Kit $kit, string $sourceAbsolutePathName, string $destinationAbsolutePathName, bool $force): bool diff --git a/src/Toolkit/src/Installer/Pool.php b/src/Toolkit/src/Installer/Pool.php index 80550bf27c0..9dc89df6097 100644 --- a/src/Toolkit/src/Installer/Pool.php +++ b/src/Toolkit/src/Installer/Pool.php @@ -13,6 +13,8 @@ namespace Symfony\UX\Toolkit\Installer; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; use Symfony\UX\Toolkit\Dependency\PhpPackageDependency; use Symfony\UX\Toolkit\File; use Symfony\UX\Toolkit\Recipe\Recipe; @@ -32,10 +34,20 @@ final class Pool private array $files = []; /** - * @param array $files + * @var array */ private array $phpPackageDependencies = []; + /** + * @var array + */ + private array $npmPackageDependencies = []; + + /** + * @var array + */ + private array $importmapPackageDependencies = []; + public function addFile(Recipe $recipe, File $file): void { $this->files[$recipe->absolutePath][$file->destinationRelativePathName] ??= $file; @@ -51,9 +63,7 @@ public function getFiles(): array public function addPhpPackageDependency(PhpPackageDependency $dependency): void { - if (isset($this->phpPackageDependencies[$dependency->name]) && $dependency->isHigherThan($this->phpPackageDependencies[$dependency->name])) { - $this->phpPackageDependencies[$dependency->name] = $dependency; - + if (isset($this->phpPackageDependencies[$dependency->name]) && !$dependency->isHigherThan($this->phpPackageDependencies[$dependency->name])) { return; } @@ -67,4 +77,34 @@ public function getPhpPackageDependencies(): array { return $this->phpPackageDependencies; } + + public function addNpmPackageDependency(NpmPackageDependency $dependency): void + { + if (isset($this->npmPackageDependencies[$dependency->name]) && !$dependency->isHigherThan($this->npmPackageDependencies[$dependency->name])) { + return; + } + + $this->npmPackageDependencies[$dependency->name] = $dependency; + } + + /** + * @return array + */ + public function getNpmPackageDependencies(): array + { + return $this->npmPackageDependencies; + } + + public function addImportmapPackageDependency(ImportmapPackageDependency $dependency): void + { + $this->importmapPackageDependencies[$dependency->package] = $dependency; + } + + /** + * @return array + */ + public function getImportmapPackageDependencies(): array + { + return $this->importmapPackageDependencies; + } } diff --git a/src/Toolkit/src/Installer/PoolResolver.php b/src/Toolkit/src/Installer/PoolResolver.php index 67736ff71e0..f69e983da73 100644 --- a/src/Toolkit/src/Installer/PoolResolver.php +++ b/src/Toolkit/src/Installer/PoolResolver.php @@ -13,6 +13,8 @@ namespace Symfony\UX\Toolkit\Installer; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; use Symfony\UX\Toolkit\Dependency\PhpPackageDependency; use Symfony\UX\Toolkit\Dependency\RecipeDependency; use Symfony\UX\Toolkit\Kit\Kit; @@ -50,6 +52,10 @@ public function resolveForRecipe(Kit $kit, Recipe $recipe): Pool foreach ($currentRecipe->manifest->dependencies as $dependency) { if ($dependency instanceof PhpPackageDependency) { $pool->addPhpPackageDependency($dependency); + } elseif ($dependency instanceof NpmPackageDependency) { + $pool->addNpmPackageDependency($dependency); + } elseif ($dependency instanceof ImportmapPackageDependency) { + $pool->addImportmapPackageDependency($dependency); } elseif ($dependency instanceof RecipeDependency) { if (null === $recipeDependency = $kit->getRecipe($dependency->name)) { throw new \LogicException(\sprintf('The recipe "%s" has a dependency on unregistered recipe "%s".', $currentRecipe->manifest->name, $dependency->name)); diff --git a/src/Toolkit/src/Recipe/RecipeManifest.php b/src/Toolkit/src/Recipe/RecipeManifest.php index 3cde06f4f68..40ccc0accba 100644 --- a/src/Toolkit/src/Recipe/RecipeManifest.php +++ b/src/Toolkit/src/Recipe/RecipeManifest.php @@ -13,6 +13,8 @@ use Symfony\Component\Filesystem\Path; use Symfony\UX\Toolkit\Dependency\DependencyInterface; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; use Symfony\UX\Toolkit\Dependency\PhpPackageDependency; use Symfony\UX\Toolkit\Dependency\RecipeDependency; use Symfony\UX\Toolkit\Dependency\Version; @@ -55,41 +57,44 @@ public static function fromJson(string $json): self { $data = json_decode($json, true, flags: \JSON_THROW_ON_ERROR); + $type = $data['type'] ?? throw new \InvalidArgumentException('Property "type" is required.'); + if (null === $type = RecipeType::tryFrom($type)) { + throw new \InvalidArgumentException(\sprintf('The recipe type "%s" is not supported.', $data['type'])); + } + $dependencies = []; foreach ($data['dependencies'] ?? [] as $i => $dependency) { if (!\is_array($dependency)) { throw new \InvalidArgumentException('Each dependency must be an associative array.'); } if (!isset($dependency['type'])) { - throw new \InvalidArgumentException(\sprintf('The dependency type is missing for dependency #%d, add "type" key.', $i)); + throw new \InvalidArgumentException(\sprintf('The dependency #%d is missing "type" field.', $i)); } if ('php' === $dependency['type']) { - $package = $dependency['package'] ?? throw new \InvalidArgumentException(\sprintf('The package name is missing for dependency #%d, add "package" key.', $i)); - if (str_contains($package, ':')) { - [$name, $version] = explode(':', $package, 2); - $dependencies[] = new PhpPackageDependency($name, new Version($version)); - } else { - $dependencies[] = new PhpPackageDependency($package); - } + $name = $dependency['name'] ?? throw new \InvalidArgumentException(\sprintf('The dependency #%d of type "php" is missing "name" field.', $i)); + $version = $dependency['version'] ?? null; + $dependencies[] = new PhpPackageDependency($name, null === $version ? null : new Version($version)); } elseif ('recipe' === $dependency['type']) { - $name = $dependency['name'] ?? throw new \InvalidArgumentException(\sprintf('The recipe name is missing for dependency #%d, add "name" key.', $i)); + $name = $dependency['name'] ?? throw new \InvalidArgumentException(\sprintf('The dependency #%d of type "recipe" is missing "name" field.', $i)); $dependencies[] = new RecipeDependency($name); + } elseif ('npm' === $dependency['type']) { + $name = $dependency['name'] ?? throw new \InvalidArgumentException(\sprintf('The dependency #%d of type "npm" is missing "name" field.', $i)); + $version = $dependency['version'] ?? null; + $dependencies[] = new NpmPackageDependency($name, null === $version ? null : new Version($version)); + } elseif ('importmap' === $dependency['type']) { + $package = $dependency['package'] ?? throw new \InvalidArgumentException(\sprintf('The dependency #%d of type "importmap" is missing "package" field.', $i)); + $dependencies[] = new ImportmapPackageDependency($package); } else { throw new \InvalidArgumentException(\sprintf('The dependency type "%s" is not supported.', $dependency['type'])); } } - $type = $data['type'] ?? throw new \InvalidArgumentException('Property "type" is required.'); - if (null === $type = RecipeType::tryFrom($type)) { - throw new \InvalidArgumentException(\sprintf('The recipe type "%s" is not supported.', $data['type'])); - } - return new self( type: $type, name: $data['name'] ?? throw new \InvalidArgumentException('Property "name" is required.'), description: $data['description'] ?? throw new \InvalidArgumentException('Property "description" is required.'), - copyFiles: $data['copy-files'] ?? throw new \InvalidArgumentException('Property "copy-files" is required.'), + copyFiles: $data['copy-files'] ?? [], dependencies: $dependencies, ); } diff --git a/src/Toolkit/tests/AssertTest.php b/src/Toolkit/tests/AssertTest.php index 37998129e63..86da179bb9b 100644 --- a/src/Toolkit/tests/AssertTest.php +++ b/src/Toolkit/tests/AssertTest.php @@ -177,4 +177,51 @@ public static function provideInvalidPhpPackageNames(): iterable yield ['twig/html-extra/']; yield ['twig/html-extra/twig']; } + + /** + * @dataProvider provideValidNpmPackageNames + */ + public function testValidNpmPackageName(string $name) + { + $this->expectNotToPerformAssertions(); + + Assert::npmPackageName($name); + } + + public static function provideValidNpmPackageNames(): iterable + { + yield ['react']; + yield ['@babel/core']; + yield ['lodash']; + yield ['@types/node']; + yield ['my-package']; + yield ['my_package']; + yield ['my.package']; + yield ['my-package123']; + yield ['@scope/my-package']; + yield ['~foo']; + } + + /** + * @dataProvider provideInvalidNpmPackageNames + */ + public function testInvalidNpmPackageName(string $name) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(\sprintf('Invalid NPM package name "%s".', $name)); + + Assert::npmPackageName($name); + } + + public static function provideInvalidNpmPackageNames(): iterable + { + yield ['']; + yield ['@']; + yield ['@scope/']; + yield ['my package']; + yield ['my/package']; + yield ['my@package']; + yield ['my/package/name']; + yield ['@scope//my-package']; + } } diff --git a/src/Toolkit/tests/Command/DebugKitCommandTest.php b/src/Toolkit/tests/Command/DebugKitCommandTest.php index 86a414faffa..78878d2a791 100644 --- a/src/Toolkit/tests/Command/DebugKitCommandTest.php +++ b/src/Toolkit/tests/Command/DebugKitCommandTest.php @@ -12,17 +12,18 @@ namespace Symfony\UX\Toolkit\Tests\Command; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\Filesystem\Path; +use Symfony\UX\Toolkit\Tests\TestHelperTrait; use Zenstruck\Console\Test\InteractsWithConsole; class DebugKitCommandTest extends KernelTestCase { use InteractsWithConsole; + use TestHelperTrait; - public function testShouldBeAbleToDebug() + public function testShouldBeAbleToDebugShadcnKit() { $this->bootKernel(); - $this->consoleCommand(\sprintf('ux:toolkit:debug-kit %s', Path::join(__DIR__, '/../../kits/shadcn'))) + $this->consoleCommand(\sprintf('ux:toolkit:debug-kit %s', self::getLocalKitPath('shadcn'))) ->execute() ->assertSuccessful() // Kit details @@ -52,4 +53,36 @@ public function testShouldBeAbleToDebug() '+--------------+----------------------------------------------------------------------------------+', ])); } + + public function testShouldBeAbleToDebugFixtureKitWithManyDependencies() + { + $this->bootKernel(); + $this->consoleCommand(\sprintf('ux:toolkit:debug-kit %s', self::getFixtureKitPath('with-many-dependencies'))) + ->execute() + ->assertSuccessful() + // Kit details + ->assertOutputContains('Name With many dependencies') + ->assertOutputContains('Homepage https://ux.symfony.com') + ->assertOutputContains('License MIT') + // Components details + ->assertOutputContains(implode(\PHP_EOL, [ + '+--------------+------------------------- Recipe: "Alert" ----------------------------------------+', + '| File(s) | N/A |', + '| Dependencies | twig/html-extra:^3.12.0 |', + '| | tales-from-a-dev/twig-tailwind-extra |', + '| | tailwindcss:^4.0.0 |', + '| | @hotwired/stimulus |', + '| | Button |', + '+--------------+----------------------------------------------------------------------------------+', + ])) + ->assertOutputContains(implode(\PHP_EOL, [ + '+--------------+------------------------ Recipe: "Button" ----------------------------------------+', + '| File(s) | N/A |', + '| Dependencies | twig/html-extra:^3.12.0 |', + '| | another/php-package:^2.0 |', + '| | another-npm-package:^1.0.0 |', + '| | another-importmap-package |', + '+--------------+----------------------------------------------------------------------------------+', + ])); + } } diff --git a/src/Toolkit/tests/Dependency/ImportmapPackageDependencyTest.php b/src/Toolkit/tests/Dependency/ImportmapPackageDependencyTest.php new file mode 100644 index 00000000000..aebfb898e95 --- /dev/null +++ b/src/Toolkit/tests/Dependency/ImportmapPackageDependencyTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Toolkit\Tests\Dependency; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; + +class ImportmapPackageDependencyTest extends TestCase +{ + public function testShouldBeInstantiable() + { + $dependency = new ImportmapPackageDependency('react'); + $this->assertSame('react', $dependency->package); + $this->assertSame('Importmap package "react"', $dependency->toDebug()); + $this->assertSame('react', (string) $dependency); + + $dependency = new ImportmapPackageDependency('bootstrap/dist/css/bootstrap.min.css'); + $this->assertSame('bootstrap/dist/css/bootstrap.min.css', $dependency->package); + $this->assertSame('Importmap package "bootstrap/dist/css/bootstrap.min.css"', $dependency->toDebug()); + $this->assertSame('bootstrap/dist/css/bootstrap.min.css', (string) $dependency); + } +} diff --git a/src/Toolkit/tests/Dependency/NpmPackageDependencyTest.php b/src/Toolkit/tests/Dependency/NpmPackageDependencyTest.php new file mode 100644 index 00000000000..e5378d938df --- /dev/null +++ b/src/Toolkit/tests/Dependency/NpmPackageDependencyTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Toolkit\Tests\Dependency; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; +use Symfony\UX\Toolkit\Dependency\Version; + +class NpmPackageDependencyTest extends TestCase +{ + public function testShouldBeInstantiable() + { + $dependency = new NpmPackageDependency('react'); + $this->assertSame('react', $dependency->name); + $this->assertNull($dependency->constraintVersion); + $this->assertSame('NPM package "react"', $dependency->toDebug()); + $this->assertSame('react', (string) $dependency); + + $dependency = new NpmPackageDependency('react', new Version('^18.0.0')); + $this->assertSame('react', $dependency->name); + $this->assertSame('NPM package "react:^18.0.0"', $dependency->toDebug()); + $this->assertSame('react:^18.0.0', (string) $dependency); + } + + public function testShouldFailIfPackageNameIsInvalid() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid NPM package name "/foo".'); + + new NpmPackageDependency('/foo'); + } +} diff --git a/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/Alert/manifest.json b/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/Alert/manifest.json new file mode 100644 index 00000000000..23f2ef6f210 --- /dev/null +++ b/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/Alert/manifest.json @@ -0,0 +1,30 @@ +{ + "$schema": "../../../../../schema-kit-recipe-v1.json", + "type": "component", + "name": "Alert", + "description": "Component Alert", + "dependencies": [ + { + "type": "php", + "name": "twig/html-extra", + "version": "^3.12.0" + }, + { + "type": "php", + "name": "tales-from-a-dev/twig-tailwind-extra" + }, + { + "type": "npm", + "name": "tailwindcss", + "version": "^4.0.0" + }, + { + "type": "importmap", + "package": "@hotwired/stimulus" + }, + { + "type": "recipe", + "name": "Button" + } + ] +} diff --git a/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/Button/manifest.json b/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/Button/manifest.json new file mode 100644 index 00000000000..12278e237ae --- /dev/null +++ b/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/Button/manifest.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../../../schema-kit-recipe-v1.json", + "type": "component", + "name": "Button", + "description": "Component Button", + "dependencies": [ + { + "type": "php", + "name": "twig/html-extra", + "version": "^3.12.0" + }, + { + "type": "php", + "name": "another/php-package", + "version": "^2.0" + }, + { + "type": "npm", + "name": "another-npm-package", + "version": "^1.0.0" + }, + { + "type": "importmap", + "package": "another-importmap-package" + } + ] +} diff --git a/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/manifest.json b/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/manifest.json new file mode 100644 index 00000000000..50ea36a990c --- /dev/null +++ b/src/Toolkit/tests/Fixtures/kits/with-many-dependencies/manifest.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../schema-kit-v1.json", + "name": "With many dependencies", + "description": "Kit used as a test fixture.", + "license": "MIT", + "homepage": "https://ux.symfony.com/" +} diff --git a/src/Toolkit/tests/Installer/PoolResolverTest.php b/src/Toolkit/tests/Installer/PoolResolverTest.php index f32649ecd77..bbb7c54039c 100644 --- a/src/Toolkit/tests/Installer/PoolResolverTest.php +++ b/src/Toolkit/tests/Installer/PoolResolverTest.php @@ -15,7 +15,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; +use Symfony\UX\Toolkit\Dependency\PhpPackageDependency; use Symfony\UX\Toolkit\Dependency\RecipeDependency; +use Symfony\UX\Toolkit\Dependency\Version; use Symfony\UX\Toolkit\Installer\PoolResolver; use Symfony\UX\Toolkit\Kit\KitSynchronizer; use Symfony\UX\Toolkit\Recipe\RecipeSynchronizer; @@ -79,4 +83,51 @@ public function testCanHandleCircularRecipeDependencies() $this->assertEquals(['templates/components/C.html.twig'], array_keys($pool->getFiles()[$recipeC->absolutePath])); $this->assertCount(0, $pool->getPhpPackageDependencies()); } + + public function testCanHandleAllPossibleDependencies() + { + $kitSynchronizer = new KitSynchronizer(new Filesystem(), new RecipeSynchronizer()); + $kit = self::createFixtureKit('with-many-dependencies'); + $kitSynchronizer->synchronize($kit); + + $poolResolver = new PoolResolver(); + + $recipeAlert = $kit->getRecipe('Alert'); + $recipeButton = $kit->getRecipe('Button'); + + $this->assertEquals([ + new PhpPackageDependency('twig/html-extra', new Version('^3.12.0')), + new PhpPackageDependency('tales-from-a-dev/twig-tailwind-extra'), + new NpmPackageDependency('tailwindcss', new Version('^4.0.0')), + new ImportmapPackageDependency('@hotwired/stimulus'), + new RecipeDependency('Button'), + ], $recipeAlert->manifest->dependencies); + + $this->assertEquals([ + new PhpPackageDependency('twig/html-extra', new Version('^3.12.0')), + new PhpPackageDependency('another/php-package', new Version('^2.0')), + new NpmPackageDependency('another-npm-package', new Version('^1.0.0')), + new ImportmapPackageDependency('another-importmap-package'), + ], $recipeButton->manifest->dependencies); + + $pool = $poolResolver->resolveForRecipe($kit, $recipeAlert); + + $this->assertCount(0, $pool->getFiles()); + + $this->assertEquals([ + 'twig/html-extra' => new PhpPackageDependency('twig/html-extra', new Version('^3.12.0')), + 'tales-from-a-dev/twig-tailwind-extra' => new PhpPackageDependency('tales-from-a-dev/twig-tailwind-extra'), + 'another/php-package' => new PhpPackageDependency('another/php-package', new Version('^2.0')), + ], $pool->getPhpPackageDependencies()); + + $this->assertEquals([ + 'tailwindcss' => new NpmPackageDependency('tailwindcss', new Version('^4.0.0')), + 'another-npm-package' => new NpmPackageDependency('another-npm-package', new Version('^1.0.0')), + ], $pool->getNpmPackageDependencies()); + + $this->assertEquals([ + '@hotwired/stimulus' => new ImportmapPackageDependency('@hotwired/stimulus'), + 'another-importmap-package' => new ImportmapPackageDependency('another-importmap-package'), + ], $pool->getImportmapPackageDependencies()); + } } diff --git a/src/Toolkit/tests/Installer/PoolTest.php b/src/Toolkit/tests/Installer/PoolTest.php index f5bee787279..c180d5fe658 100644 --- a/src/Toolkit/tests/Installer/PoolTest.php +++ b/src/Toolkit/tests/Installer/PoolTest.php @@ -14,6 +14,8 @@ namespace Symfony\UX\Toolkit\Tests\Installer; use PHPUnit\Framework\TestCase; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; use Symfony\UX\Toolkit\Dependency\PhpPackageDependency; use Symfony\UX\Toolkit\Dependency\Version; use Symfony\UX\Toolkit\File; @@ -91,5 +93,68 @@ public function testCanAddPhpPackageDependencyWithHigherVersion() $this->assertCount(1, $pool->getPhpPackageDependencies()); $this->assertEquals('twig/html-extra:^3.12.0', (string) $pool->getPhpPackageDependencies()['twig/html-extra']); + + $pool->addPhpPackageDependency(new PhpPackageDependency('twig/html-extra', new Version('^3.11.0'))); + + $this->assertCount(1, $pool->getPhpPackageDependencies()); + $this->assertEquals('twig/html-extra:^3.12.0', (string) $pool->getPhpPackageDependencies()['twig/html-extra']); + } + + public function testCanAddNpmPackageDependencies() + { + $pool = new Pool(); + + $pool->addNpmPackageDependency(new NpmPackageDependency('tailwindcss')); + + $this->assertCount(1, $pool->getNpmPackageDependencies()); + } + + public function testCantAddSameNpmPackageDependencyTwice() + { + $pool = new Pool(); + + $pool->addNpmPackageDependency(new NpmPackageDependency('tailwindcss')); + $pool->addNpmPackageDependency(new NpmPackageDependency('tailwindcss')); + + $this->assertCount(1, $pool->getNpmPackageDependencies()); + } + + public function testCanAddNpmPackageDependencyWithHigherVersion() + { + $pool = new Pool(); + + $pool->addNpmPackageDependency(new NpmPackageDependency('tailwindcss', new Version('^3.0.0'))); + + $this->assertCount(1, $pool->getNpmPackageDependencies()); + $this->assertEquals('tailwindcss:^3.0.0', (string) $pool->getNpmPackageDependencies()['tailwindcss']); + + $pool->addNpmPackageDependency(new NpmPackageDependency('tailwindcss', new Version('^4.0.0'))); + + $this->assertCount(1, $pool->getNpmPackageDependencies()); + $this->assertEquals('tailwindcss:^4.0.0', (string) $pool->getNpmPackageDependencies()['tailwindcss']); + + $pool->addNpmPackageDependency(new NpmPackageDependency('tailwindcss', new Version('^3.0.0'))); + + $this->assertCount(1, $pool->getNpmPackageDependencies()); + $this->assertEquals('tailwindcss:^4.0.0', (string) $pool->getNpmPackageDependencies()['tailwindcss']); + } + + public function testCanAddImportmapPackageDependencies() + { + $pool = new Pool(); + + $pool->addImportmapPackageDependency(new ImportmapPackageDependency('@hotwired/stimulus')); + + $this->assertCount(1, $pool->getImportmapPackageDependencies()); + } + + public function testCantAddSameImportmapPackageDependencyTwice() + { + $pool = new Pool(); + + $pool->addImportmapPackageDependency(new ImportmapPackageDependency('@hotwired/stimulus')); + $pool->addImportmapPackageDependency(new ImportmapPackageDependency('@hotwired/stimulus')); + + $this->assertCount(1, $pool->getImportmapPackageDependencies()); } } diff --git a/src/Toolkit/tests/Recipe/RecipeManifestTest.php b/src/Toolkit/tests/Recipe/RecipeManifestTest.php index 8f7e6a42bb1..a5002ce21eb 100644 --- a/src/Toolkit/tests/Recipe/RecipeManifestTest.php +++ b/src/Toolkit/tests/Recipe/RecipeManifestTest.php @@ -14,6 +14,8 @@ namespace Symfony\UX\Toolkit\Tests\Recipe; use PHPUnit\Framework\TestCase; +use Symfony\UX\Toolkit\Dependency\ImportmapPackageDependency; +use Symfony\UX\Toolkit\Dependency\NpmPackageDependency; use Symfony\UX\Toolkit\Dependency\PhpPackageDependency; use Symfony\UX\Toolkit\Dependency\RecipeDependency; use Symfony\UX\Toolkit\Dependency\Version; @@ -75,24 +77,28 @@ public function testFromJsonWithMissingDescription() JSON); } - public function testFromJsonWithMissingLicense() + public function testFromJsonWithInvalidDependencies() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Property "copy-files" is required.'); + $this->expectExceptionMessage('Each dependency must be an associative array.'); RecipeManifest::fromJson(<<expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Each dependency must be an associative array.'); + $this->expectExceptionMessage('The dependency #0 is missing "type" field.'); RecipeManifest::fromJson(<<expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The dependency type is missing for dependency #0, add "type" key.'); + $this->expectExceptionMessage('The dependency #0 of type "php" is missing "name" field.'); RecipeManifest::fromJson(<<expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The package name is missing for dependency #0, add "package" key.'); + $this->expectExceptionMessage('The dependency #1 of type "npm" is missing "name" field.'); RecipeManifest::fromJson(<<expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The dependency #2 of type "importmap" is missing "package" field.'); + + RecipeManifest::fromJson(<<expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The recipe name is missing for dependency #0, add "name" key.'); + $this->expectExceptionMessage('The dependency #3 of type "recipe" is missing "name" field.'); RecipeManifest::fromJson(<<assertSame('An incredible component', $manifest->description); $this->assertSame(['templates/' => 'templates/'], $manifest->copyFiles); $this->assertEquals([ + new PhpPackageDependency('tales-from-a-dev/twig-tailwind-extra', null), new PhpPackageDependency('symfony/ux-twig-component', new Version('^2.29')), + new NpmPackageDependency('tailwindcss', new Version('^4.0.0')), + new ImportmapPackageDependency('@hotwired/stimulus'), new RecipeDependency('OtherComponent'), ], $manifest->dependencies); } diff --git a/src/Toolkit/tests/TestHelperTrait.php b/src/Toolkit/tests/TestHelperTrait.php index 7c4658479f7..82b8efc10a6 100644 --- a/src/Toolkit/tests/TestHelperTrait.php +++ b/src/Toolkit/tests/TestHelperTrait.php @@ -17,6 +17,11 @@ trait TestHelperTrait { + private static function getLocalKitPath(string $kitName): string + { + return Path::join(__DIR__, '../kits', $kitName); + } + private static function createLocalKit(string $kitName): Kit { $kitPath = Path::join(__DIR__, '../kits', $kitName); @@ -24,9 +29,14 @@ private static function createLocalKit(string $kitName): Kit return new Kit($kitPath, KitManifest::fromJson(file_get_contents(Path::join($kitPath, 'manifest.json')))); } + private static function getFixtureKitPath(string $kitName): string + { + return Path::join(__DIR__, 'Fixtures/kits', $kitName); + } + private static function createFixtureKit(string $kitName): Kit { - $kitPath = Path::join(__DIR__, 'Fixtures/kits', $kitName); + $kitPath = self::getFixtureKitPath($kitName); return new Kit($kitPath, KitManifest::fromJson(file_get_contents(Path::join($kitPath, 'manifest.json')))); } diff --git a/ux.symfony.com/src/Service/Toolkit/ToolkitService.php b/ux.symfony.com/src/Service/Toolkit/ToolkitService.php index 2c7e5fc74a5..7d766145af6 100644 --- a/ux.symfony.com/src/Service/Toolkit/ToolkitService.php +++ b/ux.symfony.com/src/Service/Toolkit/ToolkitService.php @@ -105,6 +105,30 @@ public function renderInstallationSteps(ToolkitKitId $kitId, Recipe $component): $manual .= ''; } + $npmPackageDependencies = $pool->getNpmPackageDependencies(); + $importmapPackageDependencies = $pool->getImportmapPackageDependencies(); + + if ($npmPackageDependencies && $importmapPackageDependencies) { + $manual .= '
  • If necessary, install the following front dependencies:'; + $manual .= CodeBlockRenderer::highlightCode( + 'shell', + '# With npm/yarn/pnpm'.\PHP_EOL + .'$ npm install --save '.implode(' ', $npmPackageDependencies).\PHP_EOL + .'# With importmap (Symfony 6.3+)'.\PHP_EOL + .'$ php bin/console importmap:install '.implode(' ', $importmapPackageDependencies), + 'margin-bottom: 0' + ); + $manual .= '
  • '; + } elseif ($npmPackageDependencies) { + $manual .= '
  • If necessary, install the following npm dependencies:'; + $manual .= CodeBlockRenderer::highlightCode('shell', '$ npm install --save '.implode(' ', $npmPackageDependencies), 'margin-bottom: 0'); + $manual .= '
  • '; + } elseif ($importmapPackageDependencies) { + $manual .= '
  • If necessary, install the following importmap dependencies:'; + $manual .= CodeBlockRenderer::highlightCode('shell', '$ php bin/console importmap:install '.implode(' ', $importmapPackageDependencies), 'margin-bottom: 0'); + $manual .= '
  • '; + } + $manual .= '
  • And the most important, enjoy!
  • '; $manual .= ''; diff --git a/ux.symfony.com/src/Twig/Components/Toolkit/ComponentDoc.php b/ux.symfony.com/src/Twig/Components/Toolkit/ComponentDoc.php index d326e162c96..a60e759b952 100644 --- a/ux.symfony.com/src/Twig/Components/Toolkit/ComponentDoc.php +++ b/ux.symfony.com/src/Twig/Components/Toolkit/ComponentDoc.php @@ -59,7 +59,7 @@ public function getContent(): string $this->component->manifest->description, current($examples), $this->toolkitService->renderInstallationSteps($this->kitId, $this->component), - dump(preg_replace('/^```twig.*\n/', '```twig'.\PHP_EOL, current($examples))), + preg_replace('/^```twig.*\n/', '```twig'.\PHP_EOL, current($examples)), array_reduce(array_keys($examples), function (string $acc, string $exampleTitle) use ($examples) { $acc .= '### '.$exampleTitle.\PHP_EOL.$examples[$exampleTitle].\PHP_EOL;