diff --git a/composer.json b/composer.json index d83ff02..a388d4e 100644 --- a/composer.json +++ b/composer.json @@ -32,13 +32,10 @@ "symfony/css-selector": "^6.4|^7.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/phpunit-bridge": "^6.4|^7.0", - "symfony/stimulus-bundle": "^2.21", - "symfony/ux-live-component": "^2.21", - "symfony/ux-twig-component": "^2.21", + "symfony/stimulus-bundle": "~2.22", + "symfony/ux-live-component": "~2.22", + "symfony/ux-twig-component": "~2.22", "symfonycasts/sass-bundle": "^0.5.1", "symfonycasts/tailwind-bundle": "^0.5.0" - }, - "conflict": { - "symfony/ux-twig-component": "2.17" } } diff --git a/src/DependencyInjection/Compiler/StorybookRuntimeLoaderPass.php b/src/DependencyInjection/Compiler/StorybookRuntimeLoaderPass.php new file mode 100644 index 0000000..ff8baf8 --- /dev/null +++ b/src/DependencyInjection/Compiler/StorybookRuntimeLoaderPass.php @@ -0,0 +1,31 @@ + + */ +final class StorybookRuntimeLoaderPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + $twig = $container->getDefinition('storybook.twig'); + + $addRuntimeLoaderMethodCall = [ + 'addRuntimeLoader', + [new Reference('storybook.twig.runtime_loader')], + ]; + + // Prepend the Storybook runtime loader to the default Twig ones + $twig->setMethodCalls(array_merge( + [$addRuntimeLoaderMethodCall], + $twig->getMethodCalls()) + ); + } +} diff --git a/src/DependencyInjection/StorybookExtension.php b/src/DependencyInjection/StorybookExtension.php index e1e5728..6a13dab 100644 --- a/src/DependencyInjection/StorybookExtension.php +++ b/src/DependencyInjection/StorybookExtension.php @@ -13,8 +13,8 @@ use Storybook\Exception\UnauthorizedStoryException; use Storybook\Mock\ComponentProxyFactory; use Storybook\StoryRenderer; -use Storybook\Twig\StorybookEnvironment; use Storybook\Twig\StorybookEnvironmentConfigurator; +use Storybook\Twig\StorybookRuntimeLoader; use Storybook\Twig\StoryExtension; use Storybook\Twig\TwigComponentSubscriber; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; @@ -70,6 +70,8 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance $config = (new Processor())->processConfiguration($this, $configs); + $this->configureStorybookTwigEnvironment($container, $config); + // Proxy listener $container->register('storybook.listener.proxy_request', ProxyRequestListener::class) ->addTag('kernel.event_subscriber'); @@ -82,6 +84,39 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance ; // Story renderer + $container->register('storybook.story_renderer', StoryRenderer::class) + ->setArgument(0, new Reference('storybook.twig')) + ; + + // Args processors + $container->register('storybook.args_processor', StorybookArgsProcessor::class); + + // Proxy factory + $container->register('storybook.component_proxy_factory', ComponentProxyFactory::class) + ->setArgument(0, new AbstractArgument(\sprintf('Provided in "%s".', ComponentMockPass::class))); + + // Internal commands + $container->register('storybook.generate_preview_command', GeneratePreviewCommand::class) + ->setArgument(0, new Reference('twig')) + ->setArgument(1, new Reference('event_dispatcher')) + ->addTag('console.command', ['name' => 'storybook:generate-preview']) + ; + + // Init command + $container->register('storybook.init_command', StorybookInitCommand::class) + ->setArgument(0, $container->getParameter('kernel.project_dir')) + ->addTag('console.command', ['name' => 'storybook:init']); + + // Component subscriber + $container->register('storybook.twig.on_pre_render_listener', TwigComponentSubscriber::class) + ->setArgument(0, new Reference('request_stack')) + ->setArgument(1, new Reference('event_dispatcher')) + ->addTag('kernel.event_subscriber'); + } + + private function configureStorybookTwigEnvironment(ContainerBuilder $container, array $config): void + { + // Sandbox $defaultSandboxConfig = [ 'allowedTags' => ['component'], 'allowedFunctions' => ['component'], @@ -100,13 +135,15 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance ->setArgument(4, $sandboxConfig['allowedFunctions']) ; - // Storybook Twig extensions + // Storybook Twig environment $container->setDefinition('storybook.twig', new ChildDefinition('twig')) - ->setClass(StorybookEnvironment::class) - ->addMethodCall('setComponentRuntime', [new Reference('storybook.twig.component_runtime')]) ->setConfigurator([new Reference('storybook.twig.environment_configurator'), 'configure']) ; + $container->register('storybook.twig.runtime_loader', StorybookRuntimeLoader::class) + ->addMethodCall('addRuntime', [new Reference('storybook.twig.component_runtime')]) + ; + $container->register('storybook.twig.extension.sandbox', SandboxExtension::class) ->setArgument(0, new Reference('storybook.twig.security_policy')) ->addTag('storybook.twig.extension') @@ -123,42 +160,13 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance ->setArgument(2, $config['cache'] ?? false) ; - $container->setDefinition('storybook.twig.component_runtime', new ChildDefinition('.ux.twig_component.twig.component_runtime')) + $container->setDefinition('storybook.twig.component_runtime', new ChildDefinition('ux.twig_component.twig.component_runtime')) ->replaceArgument(0, new Reference('storybook.twig.component_renderer')) ; $container->setDefinition('storybook.twig.component_renderer', new ChildDefinition('ux.twig_component.component_renderer')) ->replaceArgument(0, new Reference('storybook.twig')) ; - - $container->register('storybook.story_renderer', StoryRenderer::class) - ->setArgument(0, new Reference('storybook.twig')) - ; - - // Args processors - $container->register('storybook.args_processor', StorybookArgsProcessor::class); - - // Proxy factory - $container->register('storybook.component_proxy_factory', ComponentProxyFactory::class) - ->setArgument(0, new AbstractArgument(\sprintf('Provided in "%s".', ComponentMockPass::class))); - - // Internal commands - $container->register('storybook.generate_preview_command', GeneratePreviewCommand::class) - ->setArgument(0, new Reference('twig')) - ->setArgument(1, new Reference('event_dispatcher')) - ->addTag('console.command', ['name' => 'storybook:generate-preview']) - ; - - // Init command - $container->register('storybook.init_command', StorybookInitCommand::class) - ->setArgument(0, $container->getParameter('kernel.project_dir')) - ->addTag('console.command', ['name' => 'storybook:init']); - - // Component subscriber - $container->register('storybook.twig.on_pre_render_listener', TwigComponentSubscriber::class) - ->setArgument(0, new Reference('request_stack')) - ->setArgument(1, new Reference('event_dispatcher')) - ->addTag('kernel.event_subscriber'); } public function getConfigTreeBuilder(): TreeBuilder diff --git a/src/StorybookBundle.php b/src/StorybookBundle.php index 8d4a032..77aa7f5 100644 --- a/src/StorybookBundle.php +++ b/src/StorybookBundle.php @@ -4,7 +4,9 @@ use Storybook\DependencyInjection\Compiler\ArgsProcessorPass; use Storybook\DependencyInjection\Compiler\ComponentMockPass; +use Storybook\DependencyInjection\Compiler\StorybookRuntimeLoaderPass; use Storybook\DependencyInjection\StorybookExtension; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -18,6 +20,9 @@ public function build(ContainerBuilder $container): void { $container->addCompilerPass(new ArgsProcessorPass()); $container->addCompilerPass(new ComponentMockPass()); + + // Must be run AFTER ResolveChildDefinitionPass + $container->addCompilerPass(new StorybookRuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING); } public function getContainerExtension(): ?ExtensionInterface diff --git a/src/Twig/StorybookEnvironment.php b/src/Twig/StorybookEnvironment.php deleted file mode 100644 index 25ee227..0000000 --- a/src/Twig/StorybookEnvironment.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * @internal - */ -class StorybookEnvironment extends Environment -{ - private ComponentRuntime $componentRuntime; - - public function setComponentRuntime(ComponentRuntime $componentRuntime): void - { - $this->componentRuntime = $componentRuntime; - } - - /** - * @template TRuntime - * - * @param class-string $class - * - * @return TRuntime - * - * @throws RuntimeError - */ - public function getRuntime(string $class) - { - if (ComponentRuntime::class === $class) { - return $this->componentRuntime; - } - - return parent::getRuntime($class); - } -} diff --git a/src/Twig/StorybookRuntimeLoader.php b/src/Twig/StorybookRuntimeLoader.php new file mode 100644 index 0000000..7c844e3 --- /dev/null +++ b/src/Twig/StorybookRuntimeLoader.php @@ -0,0 +1,35 @@ + + * + * @internal + */ +final class StorybookRuntimeLoader implements RuntimeLoaderInterface +{ + /** + * @var array + */ + private array $map = []; + + public function load(string $class): ?object + { + return $this->map[$class] ?? null; + } + + public function addRuntime(object $runtime): void + { + $class = $runtime::class; + if (isset($this->map[$class])) { + throw new \InvalidArgumentException(\sprintf('Runtime "%s" is already registered.', $class)); + } + + $this->map[$class] = $runtime; + } +} diff --git a/tests/Unit/DependencyInjection/Compiler/StorybookRuntimeLoaderPassTest.php b/tests/Unit/DependencyInjection/Compiler/StorybookRuntimeLoaderPassTest.php new file mode 100644 index 0000000..2159975 --- /dev/null +++ b/tests/Unit/DependencyInjection/Compiler/StorybookRuntimeLoaderPassTest.php @@ -0,0 +1,38 @@ +setDefinition('storybook.twig', $twigDefinition); + + $twigDefinition->addMethodCall( + 'addRuntimeLoader', + [new Reference('native_loader')], + ); + + $pass = new StorybookRuntimeLoaderPass(); + $pass->process($container); + + $runtimeLoaderCalls = array_filter( + $twigDefinition->getMethodCalls(), + static fn (array $call) => 'addRuntimeLoader' === $call[0], + ); + + self::assertCount(2, $runtimeLoaderCalls); + self::assertEquals( + new Reference('storybook.twig.runtime_loader'), + $runtimeLoaderCalls[0][1][0], + ); + } +} diff --git a/tests/Unit/Twig/StorybookRuntimeLoaderTest.php b/tests/Unit/Twig/StorybookRuntimeLoaderTest.php new file mode 100644 index 0000000..5ddbf89 --- /dev/null +++ b/tests/Unit/Twig/StorybookRuntimeLoaderTest.php @@ -0,0 +1,36 @@ +addRuntime($runtime); + + self::assertSame($runtime, $runtimeLoader->load(DummyRuntime::class)); + } + + public function testAddingTheSameRuntimeMultipleTimesThrowsException() + { + $runtimeLoader = new StorybookRuntimeLoader(); + + $runtime = new DummyRuntime(); + $runtimeLoader->addRuntime($runtime); + + $sameRuntime = new DummyRuntime(); + + $this->expectException(\InvalidArgumentException::class); + $runtimeLoader->addRuntime($sameRuntime); + } +} + +final class DummyRuntime +{ +}