diff --git a/src/Asset/EntrypointLookupCollection.php b/src/Asset/EntrypointLookupCollection.php new file mode 100644 index 00000000..add000d8 --- /dev/null +++ b/src/Asset/EntrypointLookupCollection.php @@ -0,0 +1,39 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\WebpackEncoreBundle\Asset; + +use Symfony\WebpackEncoreBundle\Exception\UndefinedBuildException; +use Psr\Container\ContainerInterface; + +/** + * Aggregate the different entry points configured in the container. + * + * Retrieve the EntrypointLookup instance from the given key. + * + * @final + */ +class EntrypointLookupCollection +{ + private $buildEntrypoints; + + public function __construct(ContainerInterface $buildEntrypoints) + { + $this->buildEntrypoints = $buildEntrypoints; + } + + public function getEntrypointLookup(string $buildName): EntrypointLookupInterface + { + if (!$this->buildEntrypoints->has($buildName)) { + throw new UndefinedBuildException(sprintf('Given entry point "%s" is not configured', $buildName)); + } + + return $this->buildEntrypoints->get($buildName); + } +} diff --git a/src/Asset/TagRenderer.php b/src/Asset/TagRenderer.php index d1fbc652..a7661b00 100644 --- a/src/Asset/TagRenderer.php +++ b/src/Asset/TagRenderer.php @@ -10,23 +10,39 @@ namespace Symfony\WebpackEncoreBundle\Asset; use Symfony\Component\Asset\Packages; +use Symfony\Component\DependencyInjection\ServiceLocator; final class TagRenderer { - private $entrypointLookup; + private $entrypointLookupCollection; private $packages; - public function __construct(EntrypointLookupInterface $entrypointLookup, Packages $packages) - { - $this->entrypointLookup = $entrypointLookup; + public function __construct( + $entrypointLookupCollection, + Packages $packages + ) { + if ($entrypointLookupCollection instanceof EntrypointLookupInterface) { + @trigger_error(sprintf('The "$entrypointLookupCollection" argument in method "%s()" must be an instance of EntrypointLookupCollection.', __METHOD__), E_USER_DEPRECATED); + + $this->entrypointLookupCollection = new EntrypointLookupCollection( + new ServiceLocator(['_default' => function() use ($entrypointLookupCollection) { + return $entrypointLookupCollection; + }]) + ); + } elseif ($entrypointLookupCollection instanceof EntrypointLookupCollection) { + $this->entrypointLookupCollection = $entrypointLookupCollection; + } else { + throw new \TypeError('The "$entrypointLookupCollection" argument must be an instance of EntrypointLookupCollection.'); + } + $this->packages = $packages; } - public function renderWebpackScriptTags(string $entryName, string $packageName = null): string + public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string { $scriptTags = []; - foreach ($this->entrypointLookup->getJavaScriptFiles($entryName) as $filename) { + foreach ($this->getEntrypointLookup($entrypointName)->getJavaScriptFiles($entryName) as $filename) { $scriptTags[] = sprintf( '', htmlentities($this->getAssetPath($filename, $packageName)) @@ -36,10 +52,10 @@ public function renderWebpackScriptTags(string $entryName, string $packageName = return implode('', $scriptTags); } - public function renderWebpackLinkTags(string $entryName, string $packageName = null): string + public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string { $scriptTags = []; - foreach ($this->entrypointLookup->getCssFiles($entryName) as $filename) { + foreach ($this->getEntrypointLookup($entrypointName)->getCssFiles($entryName) as $filename) { $scriptTags[] = sprintf( '', htmlentities($this->getAssetPath($filename, $packageName)) @@ -60,4 +76,9 @@ private function getAssetPath(string $assetPath, string $packageName = null): st $packageName ); } + + private function getEntrypointLookup(string $buildName): EntrypointLookupInterface + { + return $this->entrypointLookupCollection->getEntrypointLookup($buildName); + } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 829272ff..1e762541 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -12,6 +12,7 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; final class Configuration implements ConfigurationInterface { @@ -27,6 +28,19 @@ public function getConfigTreeBuilder() ->isRequired() ->info('The path where Encore is building the assets - i.e. Encore.setOutputPath()') ->end() + ->arrayNode('builds') + ->useAttributeAsKey('name') + ->scalarPrototype() + ->validate() + ->always(function ($values) { + if (isset($values['_default'])) { + throw new InvalidDefinitionException("Key '_default' can't be used as build name."); + } + + return $values; + }) + ->end() + ->end() ->end() ; diff --git a/src/DependencyInjection/WebpackEncoreExtension.php b/src/DependencyInjection/WebpackEncoreExtension.php index 2d4eba98..035cd658 100644 --- a/src/DependencyInjection/WebpackEncoreExtension.php +++ b/src/DependencyInjection/WebpackEncoreExtension.php @@ -13,6 +13,10 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup; final class WebpackEncoreExtension extends Extension { @@ -24,7 +28,23 @@ public function load(array $configs, ContainerBuilder $container) $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); + $factories = [ + '_default' => new Reference($this->entrypointFactory($container, '_default', $config['output_path'])) + ]; + foreach ($config['builds'] as $name => $path) { + $factories[$name] = new Reference($this->entrypointFactory($container, $name, $path)); + }; + $container->getDefinition('webpack_encore.entrypoint_lookup') - ->replaceArgument(0, $config['output_path'].'/entrypoints.json'); + ->replaceArgument(0, $factories['_default']); + $container->getDefinition('webpack_encore.entrypoint_lookup_collection') + ->replaceArgument(0, ServiceLocatorTagPass::register($container, $factories)); + } + + private function entrypointFactory(ContainerBuilder $container, string $name, string $path): string + { + $id = sprintf('webpack_encore.entrypoint_lookup[%s]', $name); + $container->setDefinition($id, new Definition(EntrypointLookup::class, [$path.'/entrypoints.json'])); + return $id; } } diff --git a/src/Exception/UndefinedBuildException.php b/src/Exception/UndefinedBuildException.php new file mode 100644 index 00000000..13ebfb2e --- /dev/null +++ b/src/Exception/UndefinedBuildException.php @@ -0,0 +1,14 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\WebpackEncoreBundle\Exception; + +class UndefinedBuildException extends \InvalidArgumentException +{ +} diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 9b0abce3..2c88c213 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -10,9 +10,12 @@ + + + - + @@ -23,6 +26,7 @@ + diff --git a/src/Twig/EntryFilesTwigExtension.php b/src/Twig/EntryFilesTwigExtension.php index 2ea2c249..30254cb3 100644 --- a/src/Twig/EntryFilesTwigExtension.php +++ b/src/Twig/EntryFilesTwigExtension.php @@ -34,33 +34,34 @@ public function getFunctions() ]; } - public function getWebpackJsFiles(string $entryName): array + public function getWebpackJsFiles(string $entryName, string $entrypointName = '_default'): array { - return $this->getEntrypointLookup() + return $this->getEntrypointLookup($entrypointName) ->getJavaScriptFiles($entryName); } - public function getWebpackCssFiles(string $entryName): array + public function getWebpackCssFiles(string $entryName, string $entrypointName = '_default'): array { - return $this->getEntrypointLookup() + return $this->getEntrypointLookup($entrypointName) ->getCssFiles($entryName); } - public function renderWebpackScriptTags(string $entryName, string $packageName = null): string + public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string { return $this->getTagRenderer() - ->renderWebpackScriptTags($entryName, $packageName); + ->renderWebpackScriptTags($entryName, $packageName, $entrypointName); } - public function renderWebpackLinkTags(string $entryName, string $packageName = null): string + public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string { return $this->getTagRenderer() - ->renderWebpackLinkTags($entryName, $packageName); + ->renderWebpackLinkTags($entryName, $packageName, $entrypointName); } - private function getEntrypointLookup(): EntrypointLookupInterface + private function getEntrypointLookup(string $entrypointName): EntrypointLookupInterface { - return $this->container->get('webpack_encore.entrypoint_lookup'); + return $this->container->get('webpack_encore.entrypoint_lookup_collection') + ->getEntrypointLookup($entrypointName); } private function getTagRenderer(): TagRenderer diff --git a/tests/Asset/EntrypointLookupCollectionTest.php b/tests/Asset/EntrypointLookupCollectionTest.php new file mode 100644 index 00000000..ce4506c0 --- /dev/null +++ b/tests/Asset/EntrypointLookupCollectionTest.php @@ -0,0 +1,20 @@ +getEntrypointLookup('something'); + } +} diff --git a/tests/Asset/EntrypointLookupTest.php b/tests/Asset/EntrypointLookupTest.php index 2c47d819..ce39b128 100644 --- a/tests/Asset/EntrypointLookupTest.php +++ b/tests/Asset/EntrypointLookupTest.php @@ -48,6 +48,18 @@ public function testGetJavaScriptFiles() ['file1.js', 'file2.js'], $this->entrypointLookup->getJavaScriptFiles('my_entry') ); + + $this->assertEquals( + [], + $this->entrypointLookup->getJavaScriptFiles('my_entry') + ); + + $this->entrypointLookup->reset(); + + $this->assertEquals( + ['file1.js', 'file2.js'], + $this->entrypointLookup->getJavaScriptFiles('my_entry') + ); } public function testGetJavaScriptFilesReturnsUniqueFilesOnly() @@ -79,6 +91,32 @@ public function testEmptyReturnOnValidEntryNoJsOrCssFile() ); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageContains There was a problem JSON decoding the + */ + public function testExceptionOnInvalidJson() + { + $filename = tempnam(sys_get_temp_dir(), 'WebpackEncoreBundle'); + file_put_contents($filename, "abcd"); + + $this->entrypointLookup = new EntrypointLookup($filename); + $this->entrypointLookup->getJavaScriptFiles('an_entry'); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageContains Could not find an "entrypoints" key in the + */ + public function testExceptionOnMissingEntrypointsKeyInJson() + { + $filename = tempnam(sys_get_temp_dir(), 'WebpackEncoreBundle'); + file_put_contents($filename, "{}"); + + $this->entrypointLookup = new EntrypointLookup($filename); + $this->entrypointLookup->getJavaScriptFiles('an_entry'); + } + /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Could not find the entrypoints file diff --git a/tests/Asset/TagRendererTest.php b/tests/Asset/TagRendererTest.php index b00bc716..59feda16 100644 --- a/tests/Asset/TagRendererTest.php +++ b/tests/Asset/TagRendererTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Asset\Packages; use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface; +use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollection; use Symfony\WebpackEncoreBundle\Asset\TagRenderer; class TagRendererTest extends TestCase @@ -15,6 +16,11 @@ public function testRenderScriptTags() $entrypointLookup->expects($this->once()) ->method('getJavaScriptFiles') ->willReturn(['/build/file1.js', '/build/file2.js']); + $entrypointCollection = $this->createMock(EntrypointLookupCollection::class); + $entrypointCollection->expects($this->once()) + ->method('getEntrypointLookup') + ->withConsecutive(['_default']) + ->will($this->onConsecutiveCalls($entrypointLookup)); $packages = $this->createMock(Packages::class); $packages->expects($this->exactly(2)) @@ -26,7 +32,7 @@ public function testRenderScriptTags() ->willReturnCallback(function($path) { return 'http://localhost:8080'.$path; }); - $renderer = new TagRenderer($entrypointLookup, $packages); + $renderer = new TagRenderer($entrypointCollection, $packages); $output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package'); $this->assertContains( @@ -45,6 +51,11 @@ public function testRenderScriptTagsWithBadFilename() $entrypointLookup->expects($this->once()) ->method('getJavaScriptFiles') ->willReturn(['/build/file<"bad_chars.js']); + $entrypointCollection = $this->createMock(EntrypointLookupCollection::class); + $entrypointCollection->expects($this->once()) + ->method('getEntrypointLookup') + ->withConsecutive(['_default']) + ->will($this->onConsecutiveCalls($entrypointLookup)); $packages = $this->createMock(Packages::class); $packages->expects($this->once()) @@ -52,7 +63,7 @@ public function testRenderScriptTagsWithBadFilename() ->willReturnCallback(function($path) { return 'http://localhost:8080'.$path; }); - $renderer = new TagRenderer($entrypointLookup, $packages); + $renderer = new TagRenderer($entrypointCollection, $packages); $output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package'); $this->assertContains( @@ -60,4 +71,61 @@ public function testRenderScriptTagsWithBadFilename() $output ); } + + public function testRenderScriptTagsWithinAnEntryPointCollection() + { + $entrypointLookup = $this->createMock(EntrypointLookupInterface::class); + $entrypointLookup->expects($this->once()) + ->method('getJavaScriptFiles') + ->willReturn(['/build/file1.js']); + + $secondEntrypointLookup = $this->createMock(EntrypointLookupInterface::class); + $secondEntrypointLookup->expects($this->once()) + ->method('getJavaScriptFiles') + ->willReturn(['/build/file2.js']); + $thirdEntrypointLookup = $this->createMock(EntrypointLookupInterface::class); + $thirdEntrypointLookup->expects($this->once()) + ->method('getJavaScriptFiles') + ->willReturn(['/build/file3.js']); + + $entrypointCollection = $this->createMock(EntrypointLookupCollection::class); + $entrypointCollection->expects($this->exactly(3)) + ->method('getEntrypointLookup') + ->withConsecutive(['_default'], ['second'], ['third']) + ->will($this->onConsecutiveCalls( + $entrypointLookup, + $secondEntrypointLookup, + $thirdEntrypointLookup + )); + + $packages = $this->createMock(Packages::class); + $packages->expects($this->exactly(3)) + ->method('getUrl') + ->withConsecutive( + ['/build/file1.js', 'custom_package'], + ['/build/file2.js', null], + ['/build/file3.js', 'specific_package'] + ) + ->willReturnCallback(function($path) { + return 'http://localhost:8080'.$path; + }); + $renderer = new TagRenderer($entrypointCollection, $packages); + + $output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package'); + $this->assertContains( + '', + $output + ); + $output = $renderer->renderWebpackScriptTags('my_entry', null, 'second'); + $this->assertContains( + '', + $output + ); + $output = $renderer->renderWebpackScriptTags('my_entry', 'specific_package', 'third'); + $this->assertContains( + '', + $output + ); + } + } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 5ca75443..361cb83b 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -18,12 +18,44 @@ public function testTwigIntegration() $kernel->boot(); $container = $kernel->getContainer(); - $html2 = $container->get('twig')->render('@integration_test/template.twig'); + $html1 = $container->get('twig')->render('@integration_test/template.twig'); $this->assertContains( '', + $html1 + ); + $this->assertContains( + ''. + '', + $html1 + ); + $this->assertContains( + '', + $html1 + ); + $this->assertContains( + ''. + '', + $html1 + ); + + $html2 = $container->get('twig')->render('@integration_test/manual_template.twig'); + $this->assertContains( + '', + $html2 + ); + $this->assertContains( + '', $html2 ); + } + public function testEntriesAreNotRepeteadWhenAlreadyOutputIntegration() + { + $kernel = new WebpackEncoreIntegrationTestKernel(true); + $kernel->boot(); + $container = $kernel->getContainer(); + + $html1 = $container->get('twig')->render('@integration_test/template.twig'); $html2 = $container->get('twig')->render('@integration_test/manual_template.twig'); $this->assertContains( '', @@ -34,6 +66,16 @@ public function testTwigIntegration() '', $html2 ); + // styles3.css is not repeated + $this->assertNotContains( + '', + $html2 + ); + // styles4.css is not repeated + $this->assertNotContains( + '', + $html2 + ); } } @@ -75,6 +117,9 @@ public function registerContainerConfiguration(LoaderInterface $loader) $container->loadFromExtension('webpack_encore', [ 'output_path' => __DIR__.'/fixtures/build', + 'builds' => [ + 'different_build' => __DIR__.'/fixtures/different_build' + ] ]); }); } @@ -88,4 +133,4 @@ public function getLogDir() { return sys_get_temp_dir().'/logs'.spl_object_hash($this); } -} \ No newline at end of file +} diff --git a/tests/fixtures/different_build/entrypoints.json b/tests/fixtures/different_build/entrypoints.json new file mode 100644 index 00000000..1128194b --- /dev/null +++ b/tests/fixtures/different_build/entrypoints.json @@ -0,0 +1,22 @@ +{ + "entrypoints": { + "third_entry": { + "js": [ + "build/other3.js" + ], + "css": [ + "build/styles3.css", + "build/styles4.css" + ] + }, + "next_entry": { + "js": [ + "build/other4.js" + ], + "css": [ + "build/styles3.css", + "build/styles4.css" + ] + } + } +} diff --git a/tests/fixtures/manual_template.twig b/tests/fixtures/manual_template.twig index 4cc1e129..c4111810 100644 --- a/tests/fixtures/manual_template.twig +++ b/tests/fixtures/manual_template.twig @@ -5,3 +5,11 @@ {% for cssFile in encore_entry_css_files('other_entry') %} {% endfor %} + +{% for jsFile in encore_entry_js_files('next_entry', 'different_build') %} + +{% endfor %} + +{% for cssFile in encore_entry_css_files('next_entry', 'different_build') %} + +{% endfor %} diff --git a/tests/fixtures/template.twig b/tests/fixtures/template.twig index 0a50c3b3..11b66b92 100644 --- a/tests/fixtures/template.twig +++ b/tests/fixtures/template.twig @@ -1,2 +1,4 @@ {{ encore_entry_script_tags('my_entry') }} {{ encore_entry_link_tags('my_entry') }} +{{ encore_entry_script_tags('third_entry', null, 'different_build') }} +{{ encore_entry_link_tags('third_entry', null, 'different_build') }}