Skip to content
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
8 changes: 6 additions & 2 deletions src/Toolkit/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@
"zenstruck/console-test": "^1.7",
"symfony/http-client": "6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0",
"symfony/phpunit-bridge": "^6.4|^7.0",
"vincentlanglet/twig-cs-fixer": "^3.5"
"symfony/phpunit-bridge": "^7.2",
"vincentlanglet/twig-cs-fixer": "^3.5",
"spatie/phpunit-snapshot-assertions": "^4.2.17",
"phpunit/phpunit": "^9.6.22",
"symfony/ux-icons": "^2.18",
"tales-from-a-dev/twig-tailwind-extra": "^0.4.0"
},
"bin": [
"bin/ux-toolkit-kit-create",
Expand Down
8 changes: 8 additions & 0 deletions src/Toolkit/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\UX\Toolkit\Command\DebugKitCommand;
use Symfony\UX\Toolkit\Command\InstallComponentCommand;
use Symfony\UX\Toolkit\Kit\KitContextRunner;
use Symfony\UX\Toolkit\Kit\KitFactory;
use Symfony\UX\Toolkit\Kit\KitSynchronizer;
use Symfony\UX\Toolkit\Registry\GitHubRegistry;
Expand Down Expand Up @@ -75,5 +76,12 @@
->args([
service('filesystem'),
])

->set('ux_toolkit.kit.kit_context_runner', KitContextRunner::class)
->public()
->args([
service('twig'),
service('ux.twig_component.component_factory'),
])
;
};
80 changes: 0 additions & 80 deletions src/Toolkit/kits/shadcn/docs/components/AlertDialog.md

This file was deleted.

4 changes: 1 addition & 3 deletions src/Toolkit/phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
failOnRisky="true"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="display_errors" value="1"/>
<ini name="error_reporting" value="-1"/>
<server name="APP_ENV" value="test" force="true"/>
<server name="SHELL_VERBOSITY" value="-1"/>
<server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value="max[total]=999999"/>
<server name="KERNEL_CLASS" value="Symfony\UX\Toolkit\Tests\Fixtures\Kernel"/>
</php>
Expand Down
107 changes: 107 additions & 0 deletions src/Toolkit/src/Kit/KitContextRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?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\Toolkit\Kit;

use Symfony\Component\Filesystem\Path;
use Symfony\UX\Toolkit\File\FileType;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;

/**
* @author Hugo Alliaume <[email protected]>
*
* @internal
*/
final class KitContextRunner
{
public function __construct(
private readonly \Twig\Environment $twig,
private readonly ComponentFactory $componentFactory,
) {
}

/**
* @template TResult of mixed
*
* @param callable(Kit): TResult $callback
*
* @return TResult
*/
public function runForKit(Kit $kit, callable $callback): mixed
{
$resetServices = $this->contextualizeServicesForKit($kit);

try {
return $callback($kit);
} finally {
$resetServices();
}
}

/**
* @return callable(): void Reset the services when called
*/
private function contextualizeServicesForKit(Kit $kit): callable
{
// Configure Twig
$initialTwigLoader = $this->twig->getLoader();
$this->twig->setLoader(new ChainLoader([
new FilesystemLoader(Path::join($kit->path, 'templates/components')),
$initialTwigLoader,
]));

// Configure Twig Components
$reflComponentFactory = new \ReflectionClass($this->componentFactory);

$reflComponentFactoryConfig = $reflComponentFactory->getProperty('config');
$initialComponentFactoryConfig = $reflComponentFactoryConfig->getValue($this->componentFactory);
$reflComponentFactoryConfig->setValue($this->componentFactory, []);

$reflComponentFactoryComponentTemplateFinder = $reflComponentFactory->getProperty('componentTemplateFinder');
$initialComponentFactoryComponentTemplateFinder = $reflComponentFactoryComponentTemplateFinder->getValue($this->componentFactory);
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $this->createComponentTemplateFinder($kit));

return function () use ($initialTwigLoader, $reflComponentFactoryConfig, $initialComponentFactoryConfig, $reflComponentFactoryComponentTemplateFinder, $initialComponentFactoryComponentTemplateFinder) {
$this->twig->setLoader($initialTwigLoader);
$reflComponentFactoryConfig->setValue($this->componentFactory, $initialComponentFactoryConfig);
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $initialComponentFactoryComponentTemplateFinder);
};
}

private function createComponentTemplateFinder(Kit $kit): ComponentTemplateFinderInterface
{
static $instances = [];

return $instances[$kit->name] ?? new class($kit) implements ComponentTemplateFinderInterface {
public function __construct(private readonly Kit $kit)
{
}

public function findAnonymousComponentTemplate(string $name): ?string
{
if (null === $component = $this->kit->getComponent($name)) {
throw new \RuntimeException(\sprintf('Component "%s" does not exist in kit "%s".', $name, $this->kit->name));
}

foreach ($component->files as $file) {
if (FileType::Twig === $file->type) {
return $file->relativePathName;
}
}

throw new \LogicException(\sprintf('No Twig files found for component "%s" in kit "%s", it should not happens.', $name, $this->kit->name));
}
};
}
}
9 changes: 9 additions & 0 deletions src/Toolkit/tests/Fixtures/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\UX\Icons\UXIconsBundle;
use Symfony\UX\Toolkit\UXToolkitBundle;
use Symfony\UX\TwigComponent\TwigComponentBundle;
use TalesFromADev\Twig\Extra\Tailwind\Bridge\Symfony\Bundle\TalesFromADevTwigExtraTailwindBundle;
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;

final class Kernel extends BaseKernel
{
Expand All @@ -29,6 +32,9 @@ public function registerBundles(): iterable
new FrameworkBundle(),
new TwigBundle(),
new TwigComponentBundle(),
new TwigExtraBundle(),
new UXIconsBundle(),
new TalesFromADevTwigExtraTailwindBundle(),
new UXToolkitBundle(),
];
}
Expand Down Expand Up @@ -69,6 +75,9 @@ protected function configureContainer(ContainerConfigurator $container): void

->alias('ux_toolkit.registry.registry_factory', '.ux_toolkit.registry.registry_factory')
->public()

->alias('ux_toolkit.registry.local', '.ux_toolkit.registry.local')
->public()
;
}
}
110 changes: 110 additions & 0 deletions src/Toolkit/tests/Functional/ComponentsRenderingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?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\Toolkit\Tests\Functional;

use Spatie\Snapshots\Drivers\HtmlDriver;
use Spatie\Snapshots\MatchesSnapshots;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\Finder\Finder;
use Symfony\UX\Toolkit\Asset\Component;
use Symfony\UX\Toolkit\Kit\Kit;
use Symfony\UX\Toolkit\Kit\KitFactory;
use Symfony\UX\Toolkit\Registry\LocalRegistry;

class ComponentsRenderingTest extends WebTestCase
{
use MatchesSnapshots;

private const KITS_DIR = __DIR__.'/../../kits';

/**
* @return iterable<string, string, string>
*/
public static function provideTestComponentRendering(): iterable
{
foreach (LocalRegistry::getAvailableKitsName() as $kitName) {
$kitDir = Path::join(__DIR__, '../../kits', $kitName, 'docs/components');
$docsFinder = (new Finder())->files()->name('*.md')->in($kitDir)->depth(0);

foreach ($docsFinder as $docFile) {
$componentName = $docFile->getFilenameWithoutExtension();

$codeBlockMatchesResult = preg_match_all('/```twig.*?\n(?P<code>.+?)```/s', $docFile->getContents(), $codeBlockMatches);
if (false === $codeBlockMatchesResult || 0 === $codeBlockMatchesResult) {
throw new \RuntimeException(\sprintf('No Twig code blocks found in file "%s"', $docFile->getRelativePathname()));
}

foreach ($codeBlockMatches['code'] as $i => $code) {
yield \sprintf('Kit %s, component %s, code #%d', $kitName, $componentName, $i + 1) => [$kitName, $componentName, $code];
}
}
}
}

/**
* @dataProvider provideTestComponentRendering
*/
public function testComponentRendering(string $kitName, string $componentName, string $code): void
{
$twig = self::getContainer()->get('twig');
$kitContextRunner = self::getContainer()->get('ux_toolkit.kit.kit_context_runner');

$kit = $this->instantiateKit($kitName);
$template = $twig->createTemplate($code);
$renderedCode = $kitContextRunner->runForKit($kit, fn () => $template->render());

$this->assertCodeRenderedMatchesHtmlSnapshot($kit, $kit->getComponent($componentName), $code, $renderedCode);
}

private function instantiateKit(string $kitName): Kit
{
$kitFactory = self::getContainer()->get('ux_toolkit.kit.kit_factory');

self::assertInstanceOf(KitFactory::class, $kitFactory);

return $kitFactory->createKitFromAbsolutePath(Path::join(__DIR__, '../../kits', $kitName));
}

private function assertCodeRenderedMatchesHtmlSnapshot(Kit $kit, Component $component, string $code, string $renderedCode): void
{
$info = \sprintf(<<<HTML
<!--
- Kit: %s
- Component: %s
- Code:
```twig
%s
```
- Rendered code (prettified for testing purposes, run "php vendor/bin/phpunit -d --update-snapshots" to update snapshots): -->
HTML,
$kit->name,
$component->name,
trim($code)
);

$this->assertMatchesSnapshot($renderedCode, new class($info) extends HtmlDriver {
public function __construct(private string $info)
{
}

public function serialize($data): string
{
$serialized = parent::serialize($data);
$serialized = str_replace(['<html><body>', '</body></html>'], '', $serialized);
$serialized = trim($serialized);

return $this->info."\n".$serialized;
}
});
}
}
Loading
Loading