From dbf633f70fe12053a3e1bc8079725dce6c632b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Mon, 8 Jun 2026 15:03:49 +0200 Subject: [PATCH 1/9] add extension wrapper, allow defer --- config/services.php | 3 + src/Collection/EntryCollection.php | 14 ---- src/Command/PrepareCommand.php | 31 ++------ src/DataCollector/EncoreCollector.php | 6 +- .../EncoreExtensionWrapper.php | 77 +++++++++++++++++++ .../EncoreExtensionWrapperFactory.php | 18 +++++ src/EntryPoint/EntryPoint.php | 6 +- src/EntryPoint/EntryPointsBuilder.php | 2 +- 8 files changed, 114 insertions(+), 43 deletions(-) create mode 100644 src/EncoreExtension/EncoreExtensionWrapper.php create mode 100644 src/EncoreExtension/EncoreExtensionWrapperFactory.php diff --git a/config/services.php b/config/services.php index d009c7f..82a2e52 100644 --- a/config/services.php +++ b/config/services.php @@ -11,6 +11,7 @@ use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\Asset\TemplateAsset; use HeimrichHannot\EncoreBundle\DataCollector\EncoreCollector; +use HeimrichHannot\EncoreBundle\EncoreExtension\EncoreExtensionWrapperFactory; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -55,4 +56,6 @@ $services->set(EncoreCollector::class) ->autoconfigure() ->autowire(); + + $services->set(EncoreExtensionWrapperFactory::class); }; diff --git a/src/Collection/EntryCollection.php b/src/Collection/EntryCollection.php index 76106f7..aa18640 100644 --- a/src/Collection/EntryCollection.php +++ b/src/Collection/EntryCollection.php @@ -10,14 +10,12 @@ use Contao\LayoutModel; use HeimrichHannot\EncoreBundle\Exception\NoEntrypointsException; -use HeimrichHannot\EncoreContracts\EncoreEntry; use Psr\Cache\CacheItemPoolInterface; class EntryCollection { private bool $useCache = false; private array $entries; - private array $encoreEntries; public function __construct( private readonly ConfigurationCollection $configurationCollection, @@ -29,18 +27,6 @@ public function __construct( } } - /** - * @return EncoreEntry[] - */ - public function getEncoreEntries(): array - { - if (!isset($this->encoreEntries)) { - $entries = $this->configurationCollection->getJsEntries(); - } - - return $this->encoreEntries; - } - /** * Return all encore entries (from webpack config and registered via bundle). * diff --git a/src/Command/PrepareCommand.php b/src/Command/PrepareCommand.php index e7d26fa..503b4c1 100644 --- a/src/Command/PrepareCommand.php +++ b/src/Command/PrepareCommand.php @@ -8,8 +8,8 @@ namespace HeimrichHannot\EncoreBundle\Command; -use Composer\InstalledVersions; use HeimrichHannot\EncoreBundle\Collection\ExtensionCollection; +use HeimrichHannot\EncoreBundle\EncoreExtension\EncoreExtensionWrapperFactory; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -38,6 +38,7 @@ public function __construct( private readonly KernelInterface $kernel, private readonly Environment $twig, private readonly ExtensionCollection $extensionCollection, + private readonly EncoreExtensionWrapperFactory $wrapperFactory, ) { parent::__construct(); } @@ -75,30 +76,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $extensionList = []; foreach ($this->extensionCollection->getExtensions() as $extension) { - $reflection = new \ReflectionClass($extension->getBundle()); - $bundle = $this->kernel->getBundles()[$reflection->getShortName()]; - $bundlePath = $bundle->getPath(); - if (!file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'composer.json')) { - $bundlePath = $bundlePath . \DIRECTORY_SEPARATOR . '..'; - } - if (!file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'composer.json')) { - trigger_error( - '[Encore Bundle] Could not find composer.json file for ' . $bundle->getName() . '.' - . ' Skipping EncoreExtension ' . $extension::class . '.' - ); - continue; - } - + $wrapper = $this->wrapperFactory->wrap($extension); try { - $composerData = json_decode(file_get_contents($bundlePath . '/composer.json'), null, 512, \JSON_THROW_ON_ERROR); - } catch (\JsonException) { - throw new \JsonException('composer.json of ' . $reflection->getShortName() . ' has a syntax error.'); + $bundlePath = $wrapper->getBundlePath(); + } catch (\RuntimeException $e) { + $this->io->warning($e->getMessage()); + continue; } - $bundlePath = InstalledVersions::getInstallPath($composerData->name); - - $bundlePath = rtrim((new Filesystem())->makePathRelative($bundlePath, $this->kernel->getProjectDir()), \DIRECTORY_SEPARATOR); - $preparedEntry = []; foreach ($extension->getEntries() as $entry) { $preparedEntry['name'] = $entry->getName(); @@ -111,7 +96,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $extensionDependencies = array_merge($extensionDependencies, $packageData['dependencies'] ?? []); } - $extensionList[] = [$reflection->getShortName(), $extension::class, $bundlePath]; + $extensionList[] = [$wrapper->getBundleShortName(), $extension::class, $bundlePath]; } $this->io->newLine(); diff --git a/src/DataCollector/EncoreCollector.php b/src/DataCollector/EncoreCollector.php index 3d30173..8dff982 100644 --- a/src/DataCollector/EncoreCollector.php +++ b/src/DataCollector/EncoreCollector.php @@ -4,6 +4,7 @@ use Composer\InstalledVersions; use HeimrichHannot\EncoreBundle\Collection\ExtensionCollection; +use HeimrichHannot\EncoreBundle\EncoreExtension\EncoreExtensionWrapperFactory; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoints; use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; use Symfony\Component\HttpFoundation\Request; @@ -13,6 +14,7 @@ class EncoreCollector extends AbstractDataCollector { public function __construct( private readonly ExtensionCollection $extensionCollection, + private readonly EncoreExtensionWrapperFactory $wrapperFactory, ) { } @@ -33,9 +35,9 @@ public function collect(Request $request, Response $response, ?\Throwable $excep $extensions = $this->extensionCollection->getExtensions(); $extensionEntries = []; foreach ($extensions as $extension) { - $reflection = new \ReflectionClass($extension->getBundle()); + $wrapper = $this->wrapperFactory->wrap($extension); $extensionEntries[] = [ - 'name' => $reflection->getShortName(), + 'name' => $wrapper->getBundleShortName(), 'entries' => $extension->getEntries(), ]; } diff --git a/src/EncoreExtension/EncoreExtensionWrapper.php b/src/EncoreExtension/EncoreExtensionWrapper.php new file mode 100644 index 0000000..6b31c7f --- /dev/null +++ b/src/EncoreExtension/EncoreExtensionWrapper.php @@ -0,0 +1,77 @@ +bundle)) { + $this->bundle = $this->kernel->getBundles()[$this->getReflection()->getShortName()]; + } + + return $this->bundle; + } + + private function getReflection(): \ReflectionClass + { + if (!isset($this->reflection)) { + $this->reflection = new \ReflectionClass($this->extension->getBundle()); + } + + return $this->reflection; + } + + public function getBundlePath(): string + { + if (!isset($this->bundlePath)) { + if ('App' === $this->extension->getBundle()) { + return $this->kernel->getProjectDir(); + } + + + $bundlePath = $this->getBundle()->getPath(); + if (!file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'composer.json')) { + $bundlePath = $bundlePath . \DIRECTORY_SEPARATOR . '..'; + } + if (!file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'composer.json')) { + throw new \RuntimeException( + '[Encore Bundle] Could not find composer.json file for ' . $this->getBundle()->getName() . '.' + . ' Skipping EncoreExtension ' . $this->extension::class . '.' + ); + } + + try { + $composerData = json_decode(file_get_contents($bundlePath . '/composer.json'), null, 512, \JSON_THROW_ON_ERROR); + } catch (\JsonException) { + throw new \JsonException('composer.json of ' . $this->getBundleShortName() . ' has a syntax error.'); + } + + $bundlePath = InstalledVersions::getInstallPath($composerData->name); + + $this->bundlePath = rtrim((new Filesystem())->makePathRelative($bundlePath, $this->kernel->getProjectDir()), \DIRECTORY_SEPARATOR); + } + + return $this->bundlePath; + } + + public function getBundleShortName(): string + { + return $this->getReflection()->getShortName(); + } +} \ No newline at end of file diff --git a/src/EncoreExtension/EncoreExtensionWrapperFactory.php b/src/EncoreExtension/EncoreExtensionWrapperFactory.php new file mode 100644 index 0000000..38d1c13 --- /dev/null +++ b/src/EncoreExtension/EncoreExtensionWrapperFactory.php @@ -0,0 +1,18 @@ +kernel); + } +} \ No newline at end of file diff --git a/src/EntryPoint/EntryPoint.php b/src/EntryPoint/EntryPoint.php index 31fec59..6c73e98 100644 --- a/src/EntryPoint/EntryPoint.php +++ b/src/EntryPoint/EntryPoint.php @@ -11,7 +11,7 @@ public function __construct( public readonly bool $requiresCss = false, public readonly string $origin = '', public readonly string $extension = '', - public readonly bool $defer = false, + public readonly ?bool $defer = null, ) { } @@ -19,8 +19,8 @@ public function getScriptExtraAttributes(): array { $attributes = []; - if ($this->defer) { - $attributes['defer'] = 'defer'; + if (is_bool($this->defer)) { + $attributes['defer'] = $this->defer; } return $attributes; diff --git a/src/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php index f8effd5..0346b5a 100644 --- a/src/EntryPoint/EntryPointsBuilder.php +++ b/src/EntryPoint/EntryPointsBuilder.php @@ -144,7 +144,7 @@ private function addEntryPoint(EntryPoints $entryPoints, string $name, bool $act requiresCss: (bool)($this->available[$name]['requires_css'] ?? true), origin: $origin, extension: $extension, - defer: (bool)($this->available[$name]['defer'] ?? false), + defer: $this->available[$name]['defer'] ?? null, )); } } From 7c7a7119be5947014d1296c57eac097b99b6daf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Mon, 8 Jun 2026 15:04:25 +0200 Subject: [PATCH 2/9] run ecs --- contao/dca/tl_layout.php | 2 -- src/EncoreExtension/EncoreExtensionWrapper.php | 13 +++++-------- .../EncoreExtensionWrapperFactory.php | 5 +++-- src/EntryPoint/EntryPoint.php | 2 +- src/EntryPoint/EntryPointsBuilder.php | 2 +- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/contao/dca/tl_layout.php b/contao/dca/tl_layout.php index 42efee3..da48eda 100644 --- a/contao/dca/tl_layout.php +++ b/contao/dca/tl_layout.php @@ -31,8 +31,6 @@ } catch (PaletteNotFoundException) { } - - /* * Subpalettes */ diff --git a/src/EncoreExtension/EncoreExtensionWrapper.php b/src/EncoreExtension/EncoreExtensionWrapper.php index 6b31c7f..6248028 100644 --- a/src/EncoreExtension/EncoreExtensionWrapper.php +++ b/src/EncoreExtension/EncoreExtensionWrapper.php @@ -16,8 +16,9 @@ class EncoreExtensionWrapper public function __construct( public readonly EncoreExtensionInterface $extension, - private readonly KernelInterface $kernel, - ) {} + private readonly KernelInterface $kernel, + ) { + } private function getBundle(): BundleInterface { @@ -44,16 +45,12 @@ public function getBundlePath(): string return $this->kernel->getProjectDir(); } - $bundlePath = $this->getBundle()->getPath(); if (!file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'composer.json')) { $bundlePath = $bundlePath . \DIRECTORY_SEPARATOR . '..'; } if (!file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'composer.json')) { - throw new \RuntimeException( - '[Encore Bundle] Could not find composer.json file for ' . $this->getBundle()->getName() . '.' - . ' Skipping EncoreExtension ' . $this->extension::class . '.' - ); + throw new \RuntimeException('[Encore Bundle] Could not find composer.json file for ' . $this->getBundle()->getName() . '. Skipping EncoreExtension ' . $this->extension::class . '.'); } try { @@ -74,4 +71,4 @@ public function getBundleShortName(): string { return $this->getReflection()->getShortName(); } -} \ No newline at end of file +} diff --git a/src/EncoreExtension/EncoreExtensionWrapperFactory.php b/src/EncoreExtension/EncoreExtensionWrapperFactory.php index 38d1c13..4ff69d1 100644 --- a/src/EncoreExtension/EncoreExtensionWrapperFactory.php +++ b/src/EncoreExtension/EncoreExtensionWrapperFactory.php @@ -9,10 +9,11 @@ class EncoreExtensionWrapperFactory { public function __construct( private readonly KernelInterface $kernel, - ) {} + ) { + } public function wrap(EncoreExtensionInterface $extension): EncoreExtensionWrapper { return new EncoreExtensionWrapper($extension, $this->kernel); } -} \ No newline at end of file +} diff --git a/src/EntryPoint/EntryPoint.php b/src/EntryPoint/EntryPoint.php index 6c73e98..4482f86 100644 --- a/src/EntryPoint/EntryPoint.php +++ b/src/EntryPoint/EntryPoint.php @@ -11,7 +11,7 @@ public function __construct( public readonly bool $requiresCss = false, public readonly string $origin = '', public readonly string $extension = '', - public readonly ?bool $defer = null, + public readonly ?bool $defer = null, ) { } diff --git a/src/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php index 0346b5a..7a332b6 100644 --- a/src/EntryPoint/EntryPointsBuilder.php +++ b/src/EntryPoint/EntryPointsBuilder.php @@ -141,7 +141,7 @@ private function addEntryPoint(EntryPoints $entryPoints, string $name, bool $act name: $name, active: $active, head: $this->available[$name]['head'] ?? false, - requiresCss: (bool)($this->available[$name]['requires_css'] ?? true), + requiresCss: (bool) ($this->available[$name]['requires_css'] ?? true), origin: $origin, extension: $extension, defer: $this->available[$name]['defer'] ?? null, From 6dde7b3e7e1337ef2d64a215ffffb05cfda1828e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Mon, 8 Jun 2026 16:21:41 +0200 Subject: [PATCH 3/9] update docs, fix issues --- docs/setup_project.md | 39 ++++++++++++++++++- src/Command/PrepareCommand.php | 6 ++- .../AbstractProjectEncoreExtension.php | 13 +++++++ .../EncoreExtensionWrapper.php | 7 +++- 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/EncoreExtension/AbstractProjectEncoreExtension.php diff --git a/docs/setup_project.md b/docs/setup_project.md index d8fa8e6..6784a4b 100644 --- a/docs/setup_project.md +++ b/docs/setup_project.md @@ -29,8 +29,43 @@ This document describes how to setup your project for the encore bundle. 1. Call `encoreBundles.addEntries()` -5. Optional: Add entries. - You can now add entries from your project, if you maintain your assets in your project code. The easiest way would be to just add them in your webpack.config.js. But you can also add them from configuration, see [Bundle Setup](setup_bundle.md) for more information. +[5. Optional: Add entries. + There are two ways to add encore entries: directly from your `webpack.config.js` or from encore extension. + + From `webpack.config.js` + ```js + // webpack.config.js + Encore + .addEntry('app_theme', './resources/themes/app/js/theme.js') + // ... + ``` + + You can also create a encore extension in your App. + Return `App` in the `EncoreExtensionInterface::getBundleName()` method implementation + or extend the `AbstractEncoreExtension`, which already do that for you. + + ```php + // src/Asset/EncoreExtension.php + namespace App\Asset; + + use HeimrichHannot\EncoreBundle\EncoreExtension\AbstractProjectEncoreExtension; + use HeimrichHannot\EncoreContracts\EncoreEntry; + + class EncoreExtension extends AbstractProjectEncoreExtension + { + public function getEntries(): array + { + return [ + EncoreEntry::create('app_theme', 'resources/themes/app/js/theme.js') + ->setRequiresCss(true), + EncoreEntry::create('app_stuff', 'resources/themes/app/js/stuff.js') + ->setIsHeadScript(true) + ->setDefer(true), + ]; + } + } + ``` + ## Example Config diff --git a/src/Command/PrepareCommand.php b/src/Command/PrepareCommand.php index 503b4c1..80957ea 100644 --- a/src/Command/PrepareCommand.php +++ b/src/Command/PrepareCommand.php @@ -87,7 +87,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $preparedEntry = []; foreach ($extension->getEntries() as $entry) { $preparedEntry['name'] = $entry->getName(); - $preparedEntry['file'] = '.' . \DIRECTORY_SEPARATOR . $bundlePath . \DIRECTORY_SEPARATOR . ltrim($entry->getPath(), \DIRECTORY_SEPARATOR); + $filePath = '.' . \DIRECTORY_SEPARATOR; + if ('.' !== $bundlePath) { + $filePath .= $bundlePath . \DIRECTORY_SEPARATOR; + } + $preparedEntry['file'] = $filePath . ltrim($entry->getPath(), \DIRECTORY_SEPARATOR); $encoreJsEntries[] = $preparedEntry; } diff --git a/src/EncoreExtension/AbstractProjectEncoreExtension.php b/src/EncoreExtension/AbstractProjectEncoreExtension.php new file mode 100644 index 0000000..79abbe4 --- /dev/null +++ b/src/EncoreExtension/AbstractProjectEncoreExtension.php @@ -0,0 +1,13 @@ +bundle)) { - $this->bundle = $this->kernel->getBundles()[$this->getReflection()->getShortName()]; + $this->bundle = $this->kernel->getBundles()[$this->getBundleShortName()]; } return $this->bundle; @@ -42,7 +42,7 @@ public function getBundlePath(): string { if (!isset($this->bundlePath)) { if ('App' === $this->extension->getBundle()) { - return $this->kernel->getProjectDir(); + return '.'; } $bundlePath = $this->getBundle()->getPath(); @@ -69,6 +69,9 @@ public function getBundlePath(): string public function getBundleShortName(): string { + if ('App' === $this->extension->getBundle()) { + return 'App'; + } return $this->getReflection()->getShortName(); } } From 1e11e4daede03747c523b5c0c618b74689bc5671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 9 Jun 2026 10:02:32 +0200 Subject: [PATCH 4/9] more refactoring --- src/Asset/EntrypointCollectionFactory.php | 4 +- src/Collection/ConfigurationCollection.php | 8 +-- src/Collection/EntryCollection.php | 51 ++++++++++++------- src/Command/PrepareCommand.php | 2 +- src/DataContainer/LayoutContainer.php | 2 +- .../EncoreExtensionWrapper.php | 5 ++ src/EntryPoint/EntryPoint.php | 15 ++++++ src/EntryPoint/EntryPointsBuilder.php | 12 +---- 8 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/Asset/EntrypointCollectionFactory.php b/src/Asset/EntrypointCollectionFactory.php index cab9c19..f012ece 100644 --- a/src/Asset/EntrypointCollectionFactory.php +++ b/src/Asset/EntrypointCollectionFactory.php @@ -21,7 +21,7 @@ public function __construct( public function createCollection(array $entrypoints): EntrypointCollection { $collection = new EntrypointCollection(); - if (empty($this->entryCollection->getEntries())) { + if (empty($this->entryCollection->getEntries(false))) { return $collection; } @@ -29,7 +29,7 @@ public function createCollection(array $entrypoints): EntrypointCollection if (isset($entrypoint['active']) && !$entrypoint['active'] || !isset($entrypoint['entry'])) { continue; } - if (!($entry = ArrayHelper::getArrayRowByFieldValue('name', $entrypoint['entry'], $this->entryCollection->getEntries()))) { + if (!($entry = ArrayHelper::getArrayRowByFieldValue('name', $entrypoint['entry'], $this->entryCollection->getEntries(true)))) { continue; } diff --git a/src/Collection/ConfigurationCollection.php b/src/Collection/ConfigurationCollection.php index 01e92f2..97fee61 100644 --- a/src/Collection/ConfigurationCollection.php +++ b/src/Collection/ConfigurationCollection.php @@ -39,13 +39,7 @@ public function getJsEntries(array $options = []): array foreach ($this->extensionCollection->getExtensions() as $extension) { if ($options['array']) { foreach ($extension->getEntries() as $entry) { - $entrypoints[] = [ - 'name' => $entry->getName(), - 'file' => $entry->getPath(), - 'requires_css' => $entry->getRequiresCss(), - 'head' => $entry->getIsHeadScript(), - 'defer' => $entry->defer, - ]; + $entrypoints[] = $entry->toArray(); } } else { $entrypoints = array_merge($entrypoints, $extension->getEntries()); diff --git a/src/Collection/EntryCollection.php b/src/Collection/EntryCollection.php index aa18640..b3dd3a2 100644 --- a/src/Collection/EntryCollection.php +++ b/src/Collection/EntryCollection.php @@ -10,6 +10,7 @@ use Contao\LayoutModel; use HeimrichHannot\EncoreBundle\Exception\NoEntrypointsException; +use HeimrichHannot\EncoreContracts\EncoreEntry; use Psr\Cache\CacheItemPoolInterface; class EntryCollection @@ -32,23 +33,47 @@ public function __construct( * * @throws NoEntrypointsException */ - public function getEntries(): array + public function getEntries(bool $asArray = true): array { if (!isset($this->entries)) { $this->entries = $this->mergeEntries( $this->bundleConfig['entrypoints_jsons'] ?? [], - $this->configurationCollection->getJsEntries([ - 'array' => true, - ]) + $this->configurationCollection->getJsEntries() ); } + if ($asArray) { + $result = []; + foreach ($this->entries as $entry) { + $result[] = $entry->toArray(); + } + return $result; + } + return $this->entries; } + /** + * @return string[] + * @throws NoEntrypointsException + */ + public function getEntryNames(): array + { + if (!isset($this->entryNames)) { + $entries = $this->getEntries(false); + $names = []; + foreach ($entries as $entry) { + $names[] = $entry->getName(); + } + return $names; + } + return $this->entryNames; + } + /** * @param array $entrypointJsonFiles entrypoint json files - * @param array $bundleConfigEntries Entries defined by encore bundle config + * @param EncoreEntry[] $bundleConfigEntries Entries defined by encore bundle config + * @return EncoreEntry[] * * @throws NoEntrypointsException */ @@ -59,25 +84,17 @@ private function mergeEntries(array $entrypointJsonFiles, array $bundleConfigEnt $entriesMap = []; foreach ($bundleConfigEntries as $entry) { - if (!isset($entry['name'])) { + if ('' === $entry->name) { continue; } - $entriesMap[$entry['name']] = true; + $entriesMap[$entry->name] = true; } foreach ($entrypoints as $name => $entrypoint) { // Only add entries that not already exist in the symfony config if (!isset($entriesMap[$name])) { - $newEntry = [ - 'name' => $name, - 'head' => false, - ]; - - if (isset($entrypoint['css'])) { - $newEntry['requires_css'] = true; - } - - $bundleConfigEntries[] = $newEntry; + $bundleConfigEntries[] = EncoreEntry::create($name, '') + ->setRequiresCss(isset($entrypoint['css'])); } } } diff --git a/src/Command/PrepareCommand.php b/src/Command/PrepareCommand.php index 80957ea..eac36eb 100644 --- a/src/Command/PrepareCommand.php +++ b/src/Command/PrepareCommand.php @@ -95,7 +95,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $encoreJsEntries[] = $preparedEntry; } - if (file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'package.json')) { + if (!$wrapper->isAppExtension() && file_exists($bundlePath . \DIRECTORY_SEPARATOR . 'package.json')) { $packageData = json_decode(file_get_contents($bundlePath . \DIRECTORY_SEPARATOR . 'package.json'), true); $extensionDependencies = array_merge($extensionDependencies, $packageData['dependencies'] ?? []); } diff --git a/src/DataContainer/LayoutContainer.php b/src/DataContainer/LayoutContainer.php index 884d20b..f5dd4f3 100644 --- a/src/DataContainer/LayoutContainer.php +++ b/src/DataContainer/LayoutContainer.php @@ -46,7 +46,7 @@ public function onLoadCallback(?DataContainer $dc = null): void $messageAdapter->addError($messageAdapter->generateUnwrapped('huh.encore.error.noEntryPoints', true)); } else { try { - $this->entryCollection->getEntries(); + $this->entryCollection->getEntries(false); } catch (NoEntrypointsException $e) { $messageAdapter->addError('[Encore Bundle] ' . $this->translator->trans('huh.encore.errors.noEntrypoints') . ' ' . $e->getMessage()); } diff --git a/src/EncoreExtension/EncoreExtensionWrapper.php b/src/EncoreExtension/EncoreExtensionWrapper.php index 1524a44..5c769ef 100644 --- a/src/EncoreExtension/EncoreExtensionWrapper.php +++ b/src/EncoreExtension/EncoreExtensionWrapper.php @@ -74,4 +74,9 @@ public function getBundleShortName(): string } return $this->getReflection()->getShortName(); } + + public function isAppExtension(): bool + { + return $this->getBundleShortName() === 'App'; + } } diff --git a/src/EntryPoint/EntryPoint.php b/src/EntryPoint/EntryPoint.php index 4482f86..d622b70 100644 --- a/src/EntryPoint/EntryPoint.php +++ b/src/EntryPoint/EntryPoint.php @@ -2,6 +2,8 @@ namespace HeimrichHannot\EncoreBundle\EntryPoint; +use HeimrichHannot\EncoreContracts\EncoreEntry; + class EntryPoint { public function __construct( @@ -15,6 +17,19 @@ public function __construct( ) { } + public static function fromEncoreEntry(EncoreEntry $entry, bool $active, string $origin, string $extension = ''): self + { + return new EntryPoint( + name: $entry->name, + active: $active, + head: $entry->isHeadScript, + requiresCss: $entry->requiresCss, + origin: $origin, + extension: $extension, + defer: $entry->defer, + ); + } + public function getScriptExtraAttributes(): array { $attributes = []; diff --git a/src/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php index 7a332b6..0a0c467 100644 --- a/src/EntryPoint/EntryPointsBuilder.php +++ b/src/EntryPoint/EntryPointsBuilder.php @@ -61,7 +61,7 @@ public function setCustomBag(?EntryBag $entryBag): self public function build(): EntryPoints { $entryPoints = new EntryPoints(); - $available = $this->entryCollection->getEntries(); + $available = $this->entryCollection->getEntries(false); if ([] !== $available) { $available = array_combine(array_column($available, 'name'), $available); } @@ -137,14 +137,6 @@ private function addEntryPoint(EntryPoints $entryPoints, string $name, bool $act return; } - $entryPoints->add(new EntryPoint( - name: $name, - active: $active, - head: $this->available[$name]['head'] ?? false, - requiresCss: (bool) ($this->available[$name]['requires_css'] ?? true), - origin: $origin, - extension: $extension, - defer: $this->available[$name]['defer'] ?? null, - )); + $entryPoints->add(EntryPoint::fromEncoreEntry($this->available[$name], $active, $origin, $extension)); } } From 4caa949cfb54251135072108ea018f1895a6c54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 9 Jun 2026 10:04:56 +0200 Subject: [PATCH 5/9] update dep --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3e63f58..9243bcd 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "ext-json": "*", "composer-runtime-api": "^2.0", "contao/core-bundle": "^4.13 || ^5.0", - "heimrichhannot/contao-encore-contracts": "^1.0", + "heimrichhannot/contao-encore-contracts": "^1.5", "heimrichhannot/contao-multi-column-editor-bundle": "^2.0", "heimrichhannot/contao-utils-bundle": "^2.231.0 || ^3.0", "symfony/cache": "^5.4 || ^6.0 || ^7.0", From 71eb01ab08d64efd7e6417f813520b7a8040b0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 9 Jun 2026 11:25:55 +0200 Subject: [PATCH 6/9] cleanup --- src/Collection/EntryCollection.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Collection/EntryCollection.php b/src/Collection/EntryCollection.php index b3dd3a2..77a413e 100644 --- a/src/Collection/EntryCollection.php +++ b/src/Collection/EntryCollection.php @@ -53,23 +53,6 @@ public function getEntries(bool $asArray = true): array return $this->entries; } - /** - * @return string[] - * @throws NoEntrypointsException - */ - public function getEntryNames(): array - { - if (!isset($this->entryNames)) { - $entries = $this->getEntries(false); - $names = []; - foreach ($entries as $entry) { - $names[] = $entry->getName(); - } - return $names; - } - return $this->entryNames; - } - /** * @param array $entrypointJsonFiles entrypoint json files * @param EncoreEntry[] $bundleConfigEntries Entries defined by encore bundle config From dc08f1c1e221f0ab4e37289a119e0d62a566dded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 9 Jun 2026 11:28:12 +0200 Subject: [PATCH 7/9] run ecs --- src/Collection/EntryCollection.php | 4 +++- src/Command/PrepareCommand.php | 2 +- src/EncoreExtension/AbstractProjectEncoreExtension.php | 2 +- src/EncoreExtension/EncoreExtensionWrapper.php | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Collection/EntryCollection.php b/src/Collection/EntryCollection.php index 77a413e..22e77e3 100644 --- a/src/Collection/EntryCollection.php +++ b/src/Collection/EntryCollection.php @@ -47,6 +47,7 @@ public function getEntries(bool $asArray = true): array foreach ($this->entries as $entry) { $result[] = $entry->toArray(); } + return $result; } @@ -54,8 +55,9 @@ public function getEntries(bool $asArray = true): array } /** - * @param array $entrypointJsonFiles entrypoint json files + * @param array $entrypointJsonFiles entrypoint json files * @param EncoreEntry[] $bundleConfigEntries Entries defined by encore bundle config + * * @return EncoreEntry[] * * @throws NoEntrypointsException diff --git a/src/Command/PrepareCommand.php b/src/Command/PrepareCommand.php index eac36eb..7f6d684 100644 --- a/src/Command/PrepareCommand.php +++ b/src/Command/PrepareCommand.php @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ('.' !== $bundlePath) { $filePath .= $bundlePath . \DIRECTORY_SEPARATOR; } - $preparedEntry['file'] = $filePath . ltrim($entry->getPath(), \DIRECTORY_SEPARATOR); + $preparedEntry['file'] = $filePath . ltrim($entry->getPath(), \DIRECTORY_SEPARATOR); $encoreJsEntries[] = $preparedEntry; } diff --git a/src/EncoreExtension/AbstractProjectEncoreExtension.php b/src/EncoreExtension/AbstractProjectEncoreExtension.php index 79abbe4..ffbb034 100644 --- a/src/EncoreExtension/AbstractProjectEncoreExtension.php +++ b/src/EncoreExtension/AbstractProjectEncoreExtension.php @@ -10,4 +10,4 @@ public function getBundle(): string { return 'App'; } -} \ No newline at end of file +} diff --git a/src/EncoreExtension/EncoreExtensionWrapper.php b/src/EncoreExtension/EncoreExtensionWrapper.php index 5c769ef..cc5f9b9 100644 --- a/src/EncoreExtension/EncoreExtensionWrapper.php +++ b/src/EncoreExtension/EncoreExtensionWrapper.php @@ -72,11 +72,12 @@ public function getBundleShortName(): string if ('App' === $this->extension->getBundle()) { return 'App'; } + return $this->getReflection()->getShortName(); } public function isAppExtension(): bool { - return $this->getBundleShortName() === 'App'; + return 'App' === $this->getBundleShortName(); } } From 5118706b8243fa5f1484a2b3cdff11fc76801e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 9 Jun 2026 11:35:40 +0200 Subject: [PATCH 8/9] adjust data collector template --- .../twig/data_collector/huh_encore.html.twig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contao/templates/twig/data_collector/huh_encore.html.twig b/contao/templates/twig/data_collector/huh_encore.html.twig index 5952ccd..f86da3e 100644 --- a/contao/templates/twig/data_collector/huh_encore.html.twig +++ b/contao/templates/twig/data_collector/huh_encore.html.twig @@ -84,9 +84,9 @@ Name - Active - Head - CSS + Active + Head + CSS Origin Extension @@ -96,9 +96,9 @@ {% for entry in collector.entries %} {{ entry.name }} - {{ entry.active ? 'yes' : '' }} - {{ entry.head ? 'yes' : '' }} - {{ entry.requiresCss ? 'yes' : '' }} + {{ entry.active ? '✅' : '' }} + {{ entry.head ? '✅' : '' }} + {{ entry.requiresCss ? '✅' : '' }} {{ entry.origin }} {{ entry.extension }} From b0223807deb14ac48801c8eec1ef8a881f4c561f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 9 Jun 2026 12:06:38 +0200 Subject: [PATCH 9/9] fix tests --- tests/Collection/EntryCollectionTest.php | 5 +++-- tests/DataCollector/EncoreCollectorTest.php | 20 ++++++++++++++++---- tests/EntryPoint/EntryPointsBuilderTest.php | 13 +++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/tests/Collection/EntryCollectionTest.php b/tests/Collection/EntryCollectionTest.php index 641fee7..d71082f 100644 --- a/tests/Collection/EntryCollectionTest.php +++ b/tests/Collection/EntryCollectionTest.php @@ -12,6 +12,7 @@ use HeimrichHannot\EncoreBundle\Collection\ConfigurationCollection; use HeimrichHannot\EncoreBundle\Collection\EntryCollection; use HeimrichHannot\EncoreBundle\Exception\NoEntrypointsException; +use HeimrichHannot\EncoreContracts\EncoreEntry; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; @@ -69,8 +70,8 @@ public function testGetEntries() $configurationCollection = $this->createMock(ConfigurationCollection::class); $configurationCollection->method('getJsEntries')->willReturn([ - ['name' => 'contao-acme-bundle', 'file' => 'somefile'], - ['name' => 'contao-list-bundle', 'file' => '/assets/js/list-bundle.js'], + new EncoreEntry('contao-acme-bundle', 'somefile'), + new EncoreEntry('contao-list-bundle', '/assets/js/list-bundle.js'), ]); $instance = $this->createTestInstance([ diff --git a/tests/DataCollector/EncoreCollectorTest.php b/tests/DataCollector/EncoreCollectorTest.php index 9309467..6a2a195 100644 --- a/tests/DataCollector/EncoreCollectorTest.php +++ b/tests/DataCollector/EncoreCollectorTest.php @@ -6,6 +6,8 @@ use Contao\TestCase\ContaoTestCase; use HeimrichHannot\EncoreBundle\Collection\ExtensionCollection; use HeimrichHannot\EncoreBundle\DataCollector\EncoreCollector; +use HeimrichHannot\EncoreBundle\EncoreExtension\EncoreExtensionWrapper; +use HeimrichHannot\EncoreBundle\EncoreExtension\EncoreExtensionWrapperFactory; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoint; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoints; use HeimrichHannot\EncoreBundle\HeimrichHannotEncoreBundle; @@ -24,13 +26,19 @@ public function testCollectWithEntryPoints(): void $extensionEntry = EncoreEntry::create('bundle-entry', '/build/bundle-entry.js'); $extension = $this->createMock(EncoreExtensionInterface::class); - $extension->expects($this->once())->method('getBundle')->willReturn(HeimrichHannotEncoreBundle::class); $extension->expects($this->once())->method('getEntries')->willReturn([$extensionEntry]); $extensionCollection = $this->createMock(ExtensionCollection::class); $extensionCollection->expects($this->once())->method('getExtensions')->willReturn([$extension]); - $collector = new EncoreCollector($extensionCollection); + $wrapperFactory = $this->createMock(EncoreExtensionWrapperFactory::class); + $wrapperFactory->method('wrap')->willReturnCallback(function (EncoreExtensionInterface $extension) { + $wrapper = $this->createMock(EncoreExtensionWrapper::class); + $wrapper->expects($this->once())->method('getBundleShortName')->willReturn('HeimrichHannotEncoreBundle'); + return $wrapper; + }); + + $collector = new EncoreCollector($extensionCollection, $wrapperFactory); $request = new Request(); $request->attributes->set('encore_entries', $entryPoints); @@ -60,7 +68,9 @@ public function testCollectWithoutEntryPointsDisablesCollector(): void $extensionCollection = $this->createMock(ExtensionCollection::class); $extensionCollection->expects($this->once())->method('getExtensions')->willReturn([]); - $collector = new EncoreCollector($extensionCollection); + $wrapperFactory = $this->createMock(EncoreExtensionWrapperFactory::class); + + $collector = new EncoreCollector($extensionCollection, $wrapperFactory); $collector->collect(new Request(), new Response()); $this->assertFalse($collector->isEnabled()); @@ -73,7 +83,9 @@ public function testCollectWithInvalidEntryPointsAttributeDisablesCollector(): v $extensionCollection = $this->createMock(ExtensionCollection::class); $extensionCollection->expects($this->once())->method('getExtensions')->willReturn([]); - $collector = new EncoreCollector($extensionCollection); + $wrapperFactory = $this->createMock(EncoreExtensionWrapperFactory::class); + + $collector = new EncoreCollector($extensionCollection, $wrapperFactory); $request = new Request(); $request->attributes->set('encore_entries', 'invalid'); diff --git a/tests/EntryPoint/EntryPointsBuilderTest.php b/tests/EntryPoint/EntryPointsBuilderTest.php index e38cfbf..4a21663 100644 --- a/tests/EntryPoint/EntryPointsBuilderTest.php +++ b/tests/EntryPoint/EntryPointsBuilderTest.php @@ -14,6 +14,7 @@ use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointsBuilder; use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; use HeimrichHannot\EncoreBundle\Request\ResponseContext\EntryBag; +use HeimrichHannot\EncoreContracts\EncoreEntry; use HeimrichHannot\TestUtilitiesBundle\Mock\ModelMockTrait; use HeimrichHannot\UtilsBundle\Util\ModelUtil; use HeimrichHannot\UtilsBundle\Util\Utils; @@ -77,11 +78,11 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void $entryCollection->expects($this->once()) ->method('getEntries') ->willReturn([ - ['name' => 'frontend-entry', 'requires_css' => false], - ['name' => 'layout-entry', 'head' => true, 'requires_css' => true], - ['name' => 'shared-entry', 'head' => false, 'requires_css' => true], - ['name' => 'parent-entry'], - ['name' => 'page-entry', 'requires_css' => false], + EncoreEntry::create('frontend-entry', '')->setRequiresCss(false), + EncoreEntry::create('layout-entry', '')->setIsHeadScript(true)->setRequiresCss(true), + EncoreEntry::create('shared-entry', '')->setIsHeadScript(false)->setRequiresCss(true), + EncoreEntry::create('parent-entry', ''), + EncoreEntry::create('page-entry', '')->setRequiresCss(false), ]); $parentPage = $this->mockModelObject(PageModel::class, [ @@ -159,7 +160,7 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void $this->assertFalse($all['shared-entry']->active); $this->assertSame('tl_page.3', $all['shared-entry']->origin); $this->assertArrayNotHasKey('shared-entry', $active); - $this->assertTrue($all['parent-entry']->requiresCss); + $this->assertFalse($all['parent-entry']->requiresCss); $this->assertSame('tl_page.2', $all['parent-entry']->origin); $this->assertFalse($all['page-entry']->head); $this->assertFalse($all['page-entry']->requiresCss);