Skip to content

Commit 2f505ad

Browse files
committed
Fix component runtime for UX 2.22
1 parent 8c0ca89 commit 2f505ad

File tree

8 files changed

+191
-85
lines changed

8 files changed

+191
-85
lines changed

composer.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,10 @@
3232
"symfony/css-selector": "^6.4|^7.0",
3333
"symfony/framework-bundle": "^6.4|^7.0",
3434
"symfony/phpunit-bridge": "^6.4|^7.0",
35-
"symfony/stimulus-bundle": "^2.21",
36-
"symfony/ux-live-component": "^2.21",
37-
"symfony/ux-twig-component": "^2.21",
35+
"symfony/stimulus-bundle": "~2.22",
36+
"symfony/ux-live-component": "~2.22",
37+
"symfony/ux-twig-component": "~2.22",
3838
"symfonycasts/sass-bundle": "^0.5.1",
3939
"symfonycasts/tailwind-bundle": "^0.5.0"
40-
},
41-
"conflict": {
42-
"symfony/ux-twig-component": "2.17"
4340
}
4441
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Storybook\DependencyInjection\Compiler;
4+
5+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
6+
use Symfony\Component\DependencyInjection\ContainerBuilder;
7+
use Symfony\Component\DependencyInjection\Reference;
8+
9+
/**
10+
* Prepends custom runtime loader in Storybook Twig environment.
11+
*
12+
* @author Nicolas Rigaud <squrious@protonmail.com>
13+
*/
14+
final class StorybookRuntimeLoaderPass implements CompilerPassInterface
15+
{
16+
public function process(ContainerBuilder $container): void
17+
{
18+
$twig = $container->getDefinition('storybook.twig');
19+
20+
$addRuntimeLoaderMethodCall = [
21+
'addRuntimeLoader',
22+
[new Reference('storybook.twig.runtime_loader')],
23+
];
24+
25+
// Prepend the Storybook runtime loader to the default Twig ones
26+
$twig->setMethodCalls(\array_merge(
27+
[$addRuntimeLoaderMethodCall],
28+
$twig->getMethodCalls())
29+
);
30+
}
31+
}

src/DependencyInjection/StorybookExtension.php

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
use Storybook\Exception\UnauthorizedStoryException;
1414
use Storybook\Mock\ComponentProxyFactory;
1515
use Storybook\StoryRenderer;
16-
use Storybook\Twig\StorybookEnvironment;
1716
use Storybook\Twig\StorybookEnvironmentConfigurator;
17+
use Storybook\Twig\StorybookRuntimeLoader;
1818
use Storybook\Twig\StoryExtension;
1919
use Storybook\Twig\TwigComponentSubscriber;
2020
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
@@ -70,6 +70,8 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance
7070

7171
$config = (new Processor())->processConfiguration($this, $configs);
7272

73+
$this->configureStorybookTwigEnvironment($container, $config);
74+
7375
// Proxy listener
7476
$container->register('storybook.listener.proxy_request', ProxyRequestListener::class)
7577
->addTag('kernel.event_subscriber');
@@ -82,6 +84,39 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance
8284
;
8385

8486
// Story renderer
87+
$container->register('storybook.story_renderer', StoryRenderer::class)
88+
->setArgument(0, new Reference('storybook.twig'))
89+
;
90+
91+
// Args processors
92+
$container->register('storybook.args_processor', StorybookArgsProcessor::class);
93+
94+
// Proxy factory
95+
$container->register('storybook.component_proxy_factory', ComponentProxyFactory::class)
96+
->setArgument(0, new AbstractArgument(\sprintf('Provided in "%s".', ComponentMockPass::class)));
97+
98+
// Internal commands
99+
$container->register('storybook.generate_preview_command', GeneratePreviewCommand::class)
100+
->setArgument(0, new Reference('twig'))
101+
->setArgument(1, new Reference('event_dispatcher'))
102+
->addTag('console.command', ['name' => 'storybook:generate-preview'])
103+
;
104+
105+
// Init command
106+
$container->register('storybook.init_command', StorybookInitCommand::class)
107+
->setArgument(0, $container->getParameter('kernel.project_dir'))
108+
->addTag('console.command', ['name' => 'storybook:init']);
109+
110+
// Component subscriber
111+
$container->register('storybook.twig.on_pre_render_listener', TwigComponentSubscriber::class)
112+
->setArgument(0, new Reference('request_stack'))
113+
->setArgument(1, new Reference('event_dispatcher'))
114+
->addTag('kernel.event_subscriber');
115+
}
116+
117+
private function configureStorybookTwigEnvironment(ContainerBuilder $container, array $config): void
118+
{
119+
// Sandbox
85120
$defaultSandboxConfig = [
86121
'allowedTags' => ['component'],
87122
'allowedFunctions' => ['component'],
@@ -90,7 +125,7 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance
90125
'allowedProperties' => [],
91126
];
92127

93-
$sandboxConfig = array_merge_recursive($defaultSandboxConfig, $config['sandbox']);
128+
$sandboxConfig = \array_merge_recursive($defaultSandboxConfig, $config['sandbox']);
94129

95130
$container->register('storybook.twig.security_policy', SecurityPolicy::class)
96131
->setArgument(0, $sandboxConfig['allowedTags'])
@@ -100,13 +135,15 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance
100135
->setArgument(4, $sandboxConfig['allowedFunctions'])
101136
;
102137

103-
// Storybook Twig extensions
138+
// Storybook Twig environment
104139
$container->setDefinition('storybook.twig', new ChildDefinition('twig'))
105-
->setClass(StorybookEnvironment::class)
106-
->addMethodCall('setComponentRuntime', [new Reference('storybook.twig.component_runtime')])
107140
->setConfigurator([new Reference('storybook.twig.environment_configurator'), 'configure'])
108141
;
109142

143+
$container->register('storybook.twig.runtime_loader', StorybookRuntimeLoader::class)
144+
->addMethodCall('addRuntime', [new Reference('storybook.twig.component_runtime')])
145+
;
146+
110147
$container->register('storybook.twig.extension.sandbox', SandboxExtension::class)
111148
->setArgument(0, new Reference('storybook.twig.security_policy'))
112149
->addTag('storybook.twig.extension')
@@ -123,42 +160,13 @@ static function (ChildDefinition $definition, AsComponentMock $attributeInstance
123160
->setArgument(2, $config['cache'] ?? false)
124161
;
125162

126-
$container->setDefinition('storybook.twig.component_runtime', new ChildDefinition('.ux.twig_component.twig.component_runtime'))
163+
$container->setDefinition('storybook.twig.component_runtime', new ChildDefinition('ux.twig_component.twig.component_runtime'))
127164
->replaceArgument(0, new Reference('storybook.twig.component_renderer'))
128165
;
129166

130167
$container->setDefinition('storybook.twig.component_renderer', new ChildDefinition('ux.twig_component.component_renderer'))
131168
->replaceArgument(0, new Reference('storybook.twig'))
132169
;
133-
134-
$container->register('storybook.story_renderer', StoryRenderer::class)
135-
->setArgument(0, new Reference('storybook.twig'))
136-
;
137-
138-
// Args processors
139-
$container->register('storybook.args_processor', StorybookArgsProcessor::class);
140-
141-
// Proxy factory
142-
$container->register('storybook.component_proxy_factory', ComponentProxyFactory::class)
143-
->setArgument(0, new AbstractArgument(\sprintf('Provided in "%s".', ComponentMockPass::class)));
144-
145-
// Internal commands
146-
$container->register('storybook.generate_preview_command', GeneratePreviewCommand::class)
147-
->setArgument(0, new Reference('twig'))
148-
->setArgument(1, new Reference('event_dispatcher'))
149-
->addTag('console.command', ['name' => 'storybook:generate-preview'])
150-
;
151-
152-
// Init command
153-
$container->register('storybook.init_command', StorybookInitCommand::class)
154-
->setArgument(0, $container->getParameter('kernel.project_dir'))
155-
->addTag('console.command', ['name' => 'storybook:init']);
156-
157-
// Component subscriber
158-
$container->register('storybook.twig.on_pre_render_listener', TwigComponentSubscriber::class)
159-
->setArgument(0, new Reference('request_stack'))
160-
->setArgument(1, new Reference('event_dispatcher'))
161-
->addTag('kernel.event_subscriber');
162170
}
163171

164172
public function getConfigTreeBuilder(): TreeBuilder

src/StorybookBundle.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use Storybook\DependencyInjection\Compiler\ArgsProcessorPass;
66
use Storybook\DependencyInjection\Compiler\ComponentMockPass;
7+
use Storybook\DependencyInjection\Compiler\StorybookRuntimeLoaderPass;
78
use Storybook\DependencyInjection\StorybookExtension;
9+
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
810
use Symfony\Component\DependencyInjection\ContainerBuilder;
911
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
1012
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -18,6 +20,9 @@ public function build(ContainerBuilder $container): void
1820
{
1921
$container->addCompilerPass(new ArgsProcessorPass());
2022
$container->addCompilerPass(new ComponentMockPass());
23+
24+
// Must be run AFTER ResolveChildDefinitionPass
25+
$container->addCompilerPass(new StorybookRuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING);
2126
}
2227

2328
public function getContainerExtension(): ?ExtensionInterface

src/Twig/StorybookEnvironment.php

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Storybook\Twig;
4+
5+
use Twig\RuntimeLoader\RuntimeLoaderInterface;
6+
7+
/**
8+
* Runtime loader for custom runtimes that have to be used in a Storybook context.
9+
*
10+
* @author Nicolas Rigaud <squrious@protonmail.com>
11+
*
12+
* @internal
13+
*/
14+
final class StorybookRuntimeLoader implements RuntimeLoaderInterface
15+
{
16+
/**
17+
* @var array<string, object>
18+
*/
19+
private array $map = [];
20+
21+
public function load(string $class): ?object
22+
{
23+
return $this->map[$class] ?? null;
24+
}
25+
26+
public function addRuntime(object $runtime): void
27+
{
28+
$class = $runtime::class;
29+
if (isset($this->map[$class])) {
30+
throw new \InvalidArgumentException(\sprintf('Runtime "%s" is already registered.', $class));
31+
}
32+
33+
$this->map[$class] = $runtime;
34+
}
35+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Storybook\Tests\Unit\DependencyInjection\Compiler;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Storybook\DependencyInjection\Compiler\StorybookRuntimeLoaderPass;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\Definition;
9+
use Symfony\Component\DependencyInjection\Reference;
10+
11+
final class StorybookRuntimeLoaderPassTest extends TestCase
12+
{
13+
public function testProcessAddsStorybookRuntimeLoaderBeforeTheOriginalOnes()
14+
{
15+
$container = new ContainerBuilder();
16+
$twigDefinition = new Definition();
17+
$container->setDefinition('storybook.twig', $twigDefinition);
18+
19+
$twigDefinition->addMethodCall(
20+
'addRuntimeLoader',
21+
[new Reference('native_loader')],
22+
);
23+
24+
$pass = new StorybookRuntimeLoaderPass();
25+
$pass->process($container);
26+
27+
$runtimeLoaderCalls = array_filter(
28+
$twigDefinition->getMethodCalls(),
29+
static fn (array $call) => 'addRuntimeLoader' === $call[0],
30+
);
31+
32+
self::assertCount(2, $runtimeLoaderCalls);
33+
self::assertEquals(
34+
new Reference('storybook.twig.runtime_loader'),
35+
$runtimeLoaderCalls[0][1][0],
36+
);
37+
}
38+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Storybook\Tests\Unit\Twig;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Storybook\Twig\StorybookRuntimeLoader;
7+
8+
final class StorybookRuntimeLoaderTest extends TestCase
9+
{
10+
public function testAddRuntime()
11+
{
12+
$runtimeLoader = new StorybookRuntimeLoader();
13+
14+
$runtime = new DummyRuntime();
15+
$runtimeLoader->addRuntime($runtime);
16+
17+
self::assertSame($runtime, $runtimeLoader->load(DummyRuntime::class));
18+
}
19+
20+
public function testAddingTheSameRuntimeMultipleTimesThrowsException()
21+
{
22+
$runtimeLoader = new StorybookRuntimeLoader();
23+
24+
$runtime = new DummyRuntime();
25+
$runtimeLoader->addRuntime($runtime);
26+
27+
$sameRuntime = new DummyRuntime();
28+
29+
$this->expectException(\InvalidArgumentException::class);
30+
$runtimeLoader->addRuntime($sameRuntime);
31+
}
32+
}
33+
34+
final class DummyRuntime
35+
{
36+
}

0 commit comments

Comments
 (0)