├── .symfony.bundle.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── doc └── index.rst └── src ├── Asset ├── EntrypointLookup.php ├── EntrypointLookupCollection.php ├── EntrypointLookupCollectionInterface.php ├── EntrypointLookupInterface.php ├── IntegrityDataProviderInterface.php └── TagRenderer.php ├── CacheWarmer └── EntrypointCacheWarmer.php ├── DependencyInjection ├── Configuration.php └── WebpackEncoreExtension.php ├── Event └── RenderAssetTagEvent.php ├── EventListener ├── ExceptionListener.php ├── PreLoadAssetsEventListener.php └── ResetAssetsEventListener.php ├── Exception ├── EntrypointNotFoundException.php └── UndefinedBuildException.php ├── Resources └── config │ └── services.xml ├── Twig └── EntryFilesTwigExtension.php └── WebpackEncoreBundle.php /.symfony.bundle.yaml: -------------------------------------------------------------------------------- 1 | branches: ["main", "2.x"] 2 | maintained_branches: ["main", "2.x"] 3 | doc_dir: "doc" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.2.0 4 | 5 | - #236 Allow entrypoints.json to be hosted remotely (@rlvdx & @Kocal) 6 | - #232 fix: correctly wire the build-time file into kernel.build_dir (@dkarlovi) 7 | - #237 Fix missing integrity hash on preload (@arnaud-ritti & @Kocal) 8 | 9 | ## v2.1.0 10 | 11 | - #233 Add support for PHP 8.3 and PHP 8.4 (@Kocal) 12 | 13 | ## v2.0.0 14 | 15 | - Minimum PHP version is now 8.1 16 | - Minimum Symfony version is now 5.4 17 | 18 | ## v1.17.0 19 | 20 | - Deprecated the `stimulus_controller()`, `stimulus_action()` and `stimulus_target` 21 | functions and related classes. Install `symfony/stimulus-bundle` instead. 22 | 23 | ## [v1.16.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.16.0) 24 | 25 | *October 18th, 2022* 26 | 27 | ### Feature 28 | 29 | - [#191](https://github.com/symfony/webpack-encore-bundle/pull/191) - Handle Stimulus CSS Classes - *@jmsche* 30 | 31 | ## [v1.15.1](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.15.1) 32 | 33 | *July 13th, 2022* 34 | 35 | ### Bug 36 | 37 | - [#189](https://github.com/symfony/webpack-encore-bundle/pull/189) - Moving deprecated code handling for stimulus_ functions into Twig extension - *@weaverryan* 38 | - [#187](https://github.com/symfony/webpack-encore-bundle/pull/187) - Improve Stimulus phpdoc - *@jmsche* 39 | - [#186](https://github.com/symfony/webpack-encore-bundle/pull/186) - Stimulus: move deprecations from DTOs to filters/functions - *@jmsche* 40 | 41 | ## [v1.15.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.15.0) 42 | 43 | *July 6th, 2022* 44 | 45 | ### Feature 46 | 47 | - [#178](https://github.com/symfony/webpack-encore-bundle/pull/178) - Add Stimulus Twig filters, handle action parameters & allow filters/functions to return array - *@jmsche* 48 | 49 | ## [v1.14.1](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.14.1) 50 | 51 | *May 3rd, 2022* 52 | 53 | ### Bug 54 | 55 | - [#172](https://github.com/symfony/webpack-encore-bundle/pull/172) - Fixing reset assets trigger on sub-requests - *@TarikAmine* 56 | - [#171](https://github.com/symfony/webpack-encore-bundle/pull/171) - Do not JSON encode stringable values - *@jderusse* 57 | 58 | ## [v1.14.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.14.0) 59 | 60 | *February 14th, 2022* 61 | 62 | ### Feature 63 | 64 | - [#147](https://github.com/symfony/webpack-encore-bundle/pull/147) - Add encore_entry_exists() twig functions to check if entrypoint has files - *@acrobat* 65 | 66 | ### Bug Fix 67 | 68 | - [#115](https://github.com/symfony/webpack-encore-bundle/pull/115) - Reset assets on FINISH_REQUEST - *@Warxcell* 69 | 70 | ## [v1.13.2](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.13.2) 71 | 72 | *December 2nd, 2021* 73 | 74 | ### Bug Fix 75 | 76 | - [#155](https://github.com/symfony/webpack-encore-bundle/pull/155) - Increase version constraint of symfony/service-contracts - *@luca-rath* 77 | 78 | ## [v1.13.1](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.13.1) 79 | 80 | *November 28th, 2021* 81 | 82 | ### Bug Fix 83 | 84 | - [#153](https://github.com/symfony/webpack-encore-bundle/pull/153) - Skipping null values from rendering - *@sadikoff* 85 | 86 | ## [v1.13.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.13.0) 87 | 88 | *November 19th, 2021* 89 | 90 | ### Feature 91 | 92 | - [#136](https://github.com/symfony/webpack-encore-bundle/pull/136) - Allow Symfony6 - *@Kocal*, *@weaverryan* 93 | 94 | ### Bug Fix 95 | 96 | - [#126](https://github.com/symfony/webpack-encore-bundle/pull/126) - Remove fallback cache on cache warmer - *@deguif* 97 | 98 | ## [v1.12.0](https://github.com/symfony/maker-bundle/releases/tag/v1.12.0) 99 | 100 | *June 18th, 2021* 101 | 102 | ### Feature 103 | 104 | - [#124](https://github.com/symfony/webpack-encore-bundle/pull/124) - feat(twig): implements stimulus_action() and stimulus_target() Twig functions, close #119 - *@Kocal* 105 | 106 | ### Bug Fix 107 | 108 | - [#111](https://github.com/symfony/webpack-encore-bundle/pull/111) - fix: fix EntrypointLookup Exception - *@jeremyFreeAgent* 109 | 110 | ## [v1.11.2](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.11.2) 111 | 112 | *April 26th, 2021* 113 | 114 | ### Bug Fix 115 | 116 | - [#122](https://github.com/symfony/webpack-encore-bundle/pull/122) - handle request deprecations - *@jrushlow* 117 | - [#121](https://github.com/symfony/webpack-encore-bundle/pull/121) - [stimulus-controller] fix bool attributes from being rendered incorrectly - *@jrushlow* 118 | 119 | ## [v1.11.1](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.11.1) 120 | 121 | *February 17th, 2021* 122 | 123 | ### Bug Fix 124 | 125 | - [#113](https://github.com/symfony/webpack-encore-bundle/pull/113) - Fixing null and false attributes - *@weaverryan* 126 | - [#112](https://github.com/symfony/webpack-encore-bundle/pull/112) - Fix the safety of the stimulus_controller function - *@stof* 127 | 128 | ## [v1.11.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.11.0) 129 | 130 | *February 10th, 2021* 131 | 132 | ### Feature 133 | 134 | - [#110](https://github.com/symfony/webpack-encore-bundle/pull/110) - Adding a simpler syntax for single stimulus controller elements - *@weaverryan* 135 | 136 | ## [v1.10.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.10.0) 137 | 138 | *February 10th, 2021* 139 | 140 | ### Feature 141 | 142 | - [#109](https://github.com/symfony/webpack-encore-bundle/pull/109) - Introduce stimulus_controller to ease Stimulus Values API usage - *@tgalopin* 143 | - [#104](https://github.com/symfony/webpack-encore-bundle/pull/104) - Allow custom EntrypointLookupCollection when instantiating the TagRenderer - *@richardhj* 144 | 145 | ## [v1.9.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.9.0) 146 | 147 | *January 15th, 2021* 148 | 149 | ### Feature 150 | 151 | - [#102](https://github.com/symfony/webpack-encore-bundle/pull/102) - Adding support for custom attributes on rendered script and link tags - *@weaverryan* 152 | 153 | ## [v1.8.0](https://github.com/symfony/webpack-encore-bundle/releases/tag/v1.8.0) 154 | 155 | *October 28th, 2020* 156 | 157 | ### Feature 158 | 159 | - [#98](https://github.com/symfony/webpack-encore-bundle/pull/98) - PHP 8.0 compatibility - *@jmsche* 160 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | When contributing, you can fix some things that will be detected by CI anyway *before* sending your pull request. 5 | 6 | The following tools will be installed in the `tools` directory, so they don't share the bundle requirements. 7 | 8 | PHPStan 9 | ------- 10 | 11 | ```bash 12 | composer install --working-dir=tools/phpstan 13 | tools/phpstan/vendor/bin/phpstan analyze 14 | # Based on the results, you may want to update the baseline 15 | tools/phpstan/vendor/bin/phpstan analyze --generate-baseline 16 | ``` 17 | 18 | PHP CS Fixer 19 | ------------ 20 | 21 | ```bash 22 | composer install --working-dir=tools/php-cs-fixer 23 | # Check what can be fixed 24 | tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --diff 25 | # Fix them 26 | tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff 27 | ``` 28 | 29 | Psalm 30 | ----- 31 | 32 | ```bash 33 | composer install --working-dir=tools/psalm 34 | tools/psalm/vendor/bin/psalm 35 | ``` 36 | 37 | PHPUnit 38 | ------- 39 | 40 | ```bash 41 | ./vendor/bin/simple-phpunit 42 | ``` 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2018 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebpackEncoreBundle: Symfony integration with Webpack Encore! 2 | ============================================================= 3 | 4 | This bundle allows you to use the `splitEntryChunks()` feature 5 | from [Webpack Encore][1] by reading an `entrypoints.json` file 6 | and helping you render all of the dynamic `script` and `link` 7 | tags needed. 8 | 9 | [Read the documentation][2] 10 | 11 | [1]: https://symfony.com/doc/current/frontend.html 12 | [2]: https://symfony.com/bundles/WebpackEncoreBundle/current/index.html 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/webpack-encore-bundle", 3 | "description": "Integration of your Symfony app with Webpack Encore", 4 | "license": "MIT", 5 | "type": "symfony-bundle", 6 | "authors": [ 7 | { 8 | "name": "Symfony Community", 9 | "homepage": "https://symfony.com/contributors" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=8.1.0", 14 | "symfony/asset": "^5.4 || ^6.2 || ^7.0", 15 | "symfony/config": "^5.4 || ^6.2 || ^7.0", 16 | "symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0", 17 | "symfony/http-kernel": "^5.4 || ^6.2 || ^7.0", 18 | "symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0" 19 | }, 20 | "require-dev": { 21 | "symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0", 22 | "symfony/http-client": "^5.4 || ^6.2 || ^7.0", 23 | "symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0", 24 | "symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0", 25 | "symfony/web-link": "^5.4 || ^6.2 || ^7.0" 26 | }, 27 | "minimum-stability": "dev", 28 | "autoload": { 29 | "psr-4": { 30 | "Symfony\\WebpackEncoreBundle\\": "src" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Symfony\\WebpackEncoreBundle\\Tests\\": "tests/" 36 | } 37 | }, 38 | "extra": { 39 | "thanks": { 40 | "name": "symfony/webpack-encore", 41 | "url": "https://github.com/symfony/webpack-encore" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | WebpackEncoreBundle: Symfony integration with Webpack Encore! 2 | ============================================================= 3 | 4 | This bundle allows you to use the ``splitEntryChunks()`` feature 5 | from `Webpack Encore`_ by reading an ``entrypoints.json`` file and 6 | helping you render all of the dynamic ``script`` and ``link`` tags 7 | needed. 8 | 9 | Installation 10 | ------------ 11 | 12 | Install the bundle with: 13 | 14 | .. code-block:: terminal 15 | 16 | $ composer require symfony/webpack-encore-bundle 17 | 18 | 19 | Configuration 20 | ------------- 21 | 22 | If you're using Symfony Flex, you're done! The recipe will 23 | pre-configure everything you need in the ``config/packages/webpack_encore.yaml`` 24 | file: 25 | 26 | .. code-block:: yaml 27 | 28 | # config/packages/webpack_encore.yaml 29 | webpack_encore: 30 | # The path where Encore is building the assets - i.e. Encore.setOutputPath() 31 | # if you customize this, you will also need to change framework.assets.json_manifest_path (it usually lives in assets.yaml) 32 | output_path: '%kernel.project_dir%/public/build' 33 | # If multiple builds are defined (as shown below), you can disable the default build: 34 | # output_path: false 35 | 36 | # Set attributes that will be rendered on all script and link tags 37 | script_attributes: 38 | defer: true 39 | # referrerpolicy: origin 40 | # link_attributes: 41 | # referrerpolicy: origin 42 | 43 | # if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') 44 | # crossorigin: 'anonymous' 45 | 46 | # preload all rendered script and link tags automatically via the http2 Link header 47 | # preload: true 48 | 49 | # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data 50 | # strict_mode: false 51 | 52 | # if you have multiple builds: 53 | # builds: 54 | # frontend: '%kernel.project_dir%/public/frontend/build' 55 | # or if you use a CDN: 56 | # frontend: 'https://cdn.example.com/frontend/build' 57 | 58 | # pass the build name" as the 3rd argument to the Twig functions 59 | # {{ encore_entry_script_tags('entry1', null, 'frontend') }} 60 | 61 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 62 | # Available in version 1.2 63 | # Put in config/packages/prod/webpack_encore.yaml 64 | # cache: true 65 | 66 | If you're not using Flex, `enable the bundle manually`_ 67 | and copy the config file from above into your project. 68 | 69 | Usage 70 | ----- 71 | 72 | The "Split Chunks" functionality in Webpack Encore is enabled by default 73 | with the recipe if you install this bundle using Symfony Flex. Otherwise, 74 | enable it manually: 75 | 76 | .. code-block:: diff 77 | 78 | // webpack.config.js 79 | // ... 80 | .setOutputPath('public/build/') 81 | .setPublicPath('/build') 82 | .setManifestKeyPrefix('build/') 83 | .addEntry('entry1', './assets/some_file.js') 84 | 85 | + .splitEntryChunks() 86 | // ... 87 | 88 | When you enable ``splitEntryChunks()``, instead of just needing 1 script tag 89 | for ``entry1.js`` and 1 link tag for ``entry1.css``, you may now need *multiple* 90 | script and link tags. This is because Webpack `"splits" your files`_ 91 | into smaller pieces for greater optimization. 92 | 93 | To help with this, Encore writes an ``entrypoints.json`` file that contains 94 | all of the files needed for each "entry". 95 | 96 | For example, to render all of the ``script`` and ``link`` tags for a specific 97 | "entry" (e.g. ``entry1``), you can: 98 | 99 | .. code-block:: twig 100 | 101 | {# any template or base layout where you need to include a JavaScript entry #} 102 | 103 | {% block javascripts %} 104 | {{ parent() }} 105 | 106 | {{ encore_entry_script_tags('entry1') }} 107 | 108 | {# or render a custom attribute #} 109 | {# 110 | {{ encore_entry_script_tags('entry1', attributes={ 111 | defer: true 112 | }) }} 113 | #} 114 | {% endblock %} 115 | 116 | {% block stylesheets %} 117 | {{ parent() }} 118 | 119 | {{ encore_entry_link_tags('entry1') }} 120 | {% endblock %} 121 | 122 | Assuming that ``entry1`` required two files to be included - ``build/vendor~entry1~entry2.js`` 123 | and ``build/entry1.js``, then ``encore_entry_script_tags()`` is equivalent to: 124 | 125 | .. code-block:: html+twig 126 | 127 | 128 | 129 | 130 | If you want more control, you can use the ``encore_entry_js_files()`` and 131 | ``encore_entry_css_files()`` methods to get the list of files needed, then 132 | loop and create the ``script`` and ``link`` tags manually. 133 | 134 | Rendering Multiple Times in a Request (e.g. to Generate a PDF) 135 | -------------------------------------------------------------- 136 | 137 | When you render your script or link tags, the bundle is smart enough 138 | not to repeat the same JavaScript or CSS file within the same request. 139 | This prevents you from having duplicate ```` or ``', 82 | $this->convertArrayToAttributes($attributes) 83 | ); 84 | 85 | $this->renderedFiles['scripts'][] = $attributes['src']; 86 | $this->renderedFilesWithAttributes['scripts'][] = $attributes; 87 | } 88 | 89 | return implode('', $scriptTags); 90 | } 91 | 92 | public function renderWebpackLinkTags(string $entryName, ?string $packageName = null, ?string $entrypointName = null, array $extraAttributes = []): string 93 | { 94 | $entrypointName = $entrypointName ?: '_default'; 95 | $scriptTags = []; 96 | $entryPointLookup = $this->getEntrypointLookup($entrypointName); 97 | $integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : []; 98 | 99 | foreach ($entryPointLookup->getCssFiles($entryName) as $filename) { 100 | $attributes = []; 101 | $attributes['rel'] = 'stylesheet'; 102 | $attributes['href'] = $this->getAssetPath($filename, $packageName); 103 | $attributes = array_merge($attributes, $this->defaultAttributes, $this->defaultLinkAttributes, $extraAttributes); 104 | 105 | if (isset($integrityHashes[$filename])) { 106 | $attributes['integrity'] = $integrityHashes[$filename]; 107 | } 108 | 109 | $event = new RenderAssetTagEvent( 110 | RenderAssetTagEvent::TYPE_LINK, 111 | $attributes['href'], 112 | $attributes 113 | ); 114 | if (null !== $this->eventDispatcher) { 115 | $this->eventDispatcher->dispatch($event); 116 | } 117 | $attributes = $event->getAttributes(); 118 | 119 | $scriptTags[] = \sprintf( 120 | '', 121 | $this->convertArrayToAttributes($attributes) 122 | ); 123 | 124 | $this->renderedFiles['styles'][] = $attributes['href']; 125 | $this->renderedFilesWithAttributes['styles'][] = $attributes; 126 | } 127 | 128 | return implode('', $scriptTags); 129 | } 130 | 131 | /** 132 | * @param bool $includeAttributes Whether to include the attributes or not. 133 | * In WebpackEncoreBundle 3.0, this parameter will be removed, 134 | * and the attributes will always be included. 135 | * TODO WebpackEncoreBundle 3.0 136 | * 137 | * @return ($includeAttributes is true ? list> : list) 138 | */ 139 | public function getRenderedScripts(bool $includeAttributes = false): array 140 | { 141 | return $includeAttributes ? $this->renderedFilesWithAttributes['scripts'] : $this->renderedFiles['scripts']; 142 | } 143 | 144 | /** 145 | * @param bool $includeAttributes Whether to include the attributes or not. 146 | * In WebpackEncoreBundle 3.0, this parameter will be removed, 147 | * and the attributes will always be included. 148 | * TODO WebpackEncoreBundle 3.0 149 | * 150 | * @return ($includeAttributes is true ? list> : list) 151 | */ 152 | public function getRenderedStyles(bool $includeAttributes = false): array 153 | { 154 | return $includeAttributes ? $this->renderedFilesWithAttributes['styles'] : $this->renderedFiles['styles']; 155 | } 156 | 157 | public function getDefaultAttributes(): array 158 | { 159 | return $this->defaultAttributes; 160 | } 161 | 162 | public function reset(): void 163 | { 164 | $this->renderedFiles = $this->renderedFilesWithAttributes = [ 165 | 'scripts' => [], 166 | 'styles' => [], 167 | ]; 168 | } 169 | 170 | private function getAssetPath(string $assetPath, ?string $packageName = null): string 171 | { 172 | if (null === $this->packages) { 173 | throw new \Exception('To render the script or link tags, run "composer require symfony/asset".'); 174 | } 175 | 176 | return $this->packages->getUrl( 177 | $assetPath, 178 | $packageName 179 | ); 180 | } 181 | 182 | private function getEntrypointLookup(string $buildName): EntrypointLookupInterface 183 | { 184 | return $this->entrypointLookupCollection->getEntrypointLookup($buildName); 185 | } 186 | 187 | private function convertArrayToAttributes(array $attributesMap): string 188 | { 189 | // remove attributes set specifically to false 190 | $attributesMap = array_filter($attributesMap, static function ($value) { 191 | return false !== $value; 192 | }); 193 | 194 | return implode(' ', array_map( 195 | static function ($key, $value) { 196 | // allows for things like defer: true to only render "defer" 197 | if (true === $value || null === $value) { 198 | return $key; 199 | } 200 | 201 | return \sprintf('%s="%s"', $key, htmlentities($value)); 202 | }, 203 | array_keys($attributesMap), 204 | $attributesMap 205 | )); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/CacheWarmer/EntrypointCacheWarmer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\CacheWarmer; 13 | 14 | use Symfony\Bundle\FrameworkBundle\CacheWarmer\AbstractPhpFileCacheWarmer; 15 | use Symfony\Component\Cache\Adapter\ArrayAdapter; 16 | use Symfony\Contracts\HttpClient\HttpClientInterface; 17 | use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup; 18 | use Symfony\WebpackEncoreBundle\Exception\EntrypointNotFoundException; 19 | 20 | class EntrypointCacheWarmer extends AbstractPhpFileCacheWarmer 21 | { 22 | private $cacheKeys; 23 | private $httpClient; 24 | 25 | public function __construct(array $cacheKeys, ?HttpClientInterface $httpClient, string $phpArrayFile) 26 | { 27 | $this->cacheKeys = $cacheKeys; 28 | $this->httpClient = $httpClient; 29 | parent::__construct($phpArrayFile); 30 | } 31 | 32 | protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?string $buildDir = null): bool 33 | { 34 | foreach ($this->cacheKeys as $cacheKey => $path) { 35 | // If the file does not exist then just skip past this entry point. 36 | if (!str_starts_with($path, 'http') && !file_exists($path)) { 37 | continue; 38 | } 39 | 40 | $entryPointLookup = new EntrypointLookup($path, $arrayAdapter, $cacheKey, httpClient: $this->httpClient); 41 | 42 | try { 43 | $entryPointLookup->getJavaScriptFiles('dummy'); 44 | } catch (EntrypointNotFoundException $e) { 45 | // ignore exception 46 | } 47 | } 48 | 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 15 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 16 | use Symfony\Component\Config\Definition\ConfigurationInterface; 17 | use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; 18 | 19 | final class Configuration implements ConfigurationInterface 20 | { 21 | public function getConfigTreeBuilder(): TreeBuilder 22 | { 23 | $treeBuilder = new TreeBuilder('webpack_encore'); 24 | /** @var ArrayNodeDefinition $rootNode */ 25 | $rootNode = $treeBuilder->getRootNode(); 26 | 27 | $rootNode 28 | ->validate() 29 | ->ifTrue(function (array $v): bool { 30 | return false === $v['output_path'] && empty($v['builds']); 31 | }) 32 | ->thenInvalid('Default build can only be disabled if multiple entry points are defined.') 33 | ->end() 34 | ->children() 35 | ->scalarNode('output_path') 36 | ->isRequired() 37 | ->info('The path where Encore is building the assets - i.e. Encore.setOutputPath()') 38 | ->end() 39 | ->enumNode('crossorigin') 40 | ->defaultFalse() 41 | ->values([false, 'anonymous', 'use-credentials']) 42 | ->info('crossorigin value when Encore.enableIntegrityHashes() is used, can be false (default), anonymous or use-credentials') 43 | ->end() 44 | ->booleanNode('preload') 45 | ->info('preload all rendered script and link tags automatically via the http2 Link header.') 46 | ->defaultFalse() 47 | ->end() 48 | ->booleanNode('cache') 49 | ->info('Enable caching of the entry point file(s)') 50 | ->defaultFalse() 51 | ->end() 52 | ->booleanNode('strict_mode') 53 | ->info('Throw an exception if the entrypoints.json file is missing or an entry is missing from the data') 54 | ->defaultTrue() 55 | ->end() 56 | ->arrayNode('builds') 57 | ->useAttributeAsKey('name') 58 | ->normalizeKeys(false) 59 | ->scalarPrototype() 60 | ->validate() 61 | ->always(function ($values) { 62 | if (isset($values['_default'])) { 63 | throw new InvalidDefinitionException("Key '_default' can't be used as build name."); 64 | } 65 | 66 | return $values; 67 | }) 68 | ->end() 69 | ->end() 70 | ->end() 71 | ->arrayNode('script_attributes') 72 | ->info('Key/value pair of attributes to render on all script tags') 73 | ->example('{ defer: true, referrerpolicy: "origin" }') 74 | ->useAttributeAsKey('name') 75 | ->normalizeKeys(false) 76 | ->scalarPrototype()->end() 77 | ->end() 78 | ->arrayNode('link_attributes') 79 | ->info('Key/value pair of attributes to render on all CSS link tags') 80 | ->example('{ referrerpolicy: "origin" }') 81 | ->useAttributeAsKey('name') 82 | ->normalizeKeys(false) 83 | ->scalarPrototype()->end() 84 | ->end() 85 | ->end() 86 | ; 87 | 88 | return $treeBuilder; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/DependencyInjection/WebpackEncoreExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\FileLocator; 15 | use Symfony\Component\DependencyInjection\Alias; 16 | use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Definition; 19 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 20 | use Symfony\Component\DependencyInjection\Reference; 21 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 22 | use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; 23 | use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup; 24 | use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface; 25 | use Symfony\WebpackEncoreBundle\EventListener\ResetAssetsEventListener; 26 | 27 | final class WebpackEncoreExtension extends Extension 28 | { 29 | private const ENTRYPOINTS_FILE_NAME = 'entrypoints.json'; 30 | 31 | public function load(array $configs, ContainerBuilder $container): void 32 | { 33 | $loader = new XmlFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); 34 | $loader->load('services.xml'); 35 | 36 | $configuration = $this->getConfiguration($configs, $container); 37 | $config = $this->processConfiguration($configuration, $configs); 38 | 39 | $factories = []; 40 | $cacheKeys = []; 41 | 42 | if (false !== $config['output_path']) { 43 | $factories['_default'] = $this->entrypointFactory($container, '_default', $config['output_path'], $config['cache'], $config['strict_mode']); 44 | $cacheKeys['_default'] = $config['output_path'].'/'.self::ENTRYPOINTS_FILE_NAME; 45 | 46 | $container->getDefinition('webpack_encore.entrypoint_lookup_collection') 47 | ->setArgument(1, '_default'); 48 | } 49 | 50 | foreach ($config['builds'] as $name => $path) { 51 | $factories[$name] = $this->entrypointFactory($container, $name, $path, $config['cache'], $config['strict_mode']); 52 | $cacheKeys[rawurlencode($name)] = $path.'/'.self::ENTRYPOINTS_FILE_NAME; 53 | } 54 | 55 | $container->getDefinition('webpack_encore.exception_listener') 56 | ->replaceArgument(1, array_keys($factories)); 57 | 58 | $container->getDefinition('webpack_encore.entrypoint_lookup.cache_warmer') 59 | ->replaceArgument(0, $cacheKeys); 60 | 61 | $container->getDefinition('webpack_encore.entrypoint_lookup_collection') 62 | ->replaceArgument(0, ServiceLocatorTagPass::register($container, $factories)); 63 | 64 | $container->getDefinition(ResetAssetsEventListener::class) 65 | ->setArgument(1, array_keys($factories)); 66 | if (false !== $config['output_path']) { 67 | $container->setAlias(EntrypointLookupInterface::class, new Alias($this->getEntrypointServiceId('_default'))); 68 | } 69 | 70 | $defaultAttributes = []; 71 | 72 | if (false !== $config['crossorigin']) { 73 | $defaultAttributes['crossorigin'] = $config['crossorigin']; 74 | } 75 | 76 | $container->getDefinition('webpack_encore.tag_renderer') 77 | ->replaceArgument(2, $defaultAttributes) 78 | ->replaceArgument(3, $config['script_attributes']) 79 | ->replaceArgument(4, $config['link_attributes']); 80 | 81 | if ($config['preload']) { 82 | if (!class_exists(AddLinkHeaderListener::class)) { 83 | throw new \LogicException('To use the "preload" option, the WebLink component must be installed. Try running "composer require symfony/web-link".'); 84 | } 85 | } else { 86 | $container->removeDefinition('webpack_encore.preload_assets_event_listener'); 87 | } 88 | } 89 | 90 | private function entrypointFactory(ContainerBuilder $container, string $name, string $path, bool $cacheEnabled, bool $strictMode): Reference 91 | { 92 | $id = $this->getEntrypointServiceId($name); 93 | $arguments = [ 94 | $path.'/'.self::ENTRYPOINTS_FILE_NAME, 95 | $cacheEnabled ? new Reference('webpack_encore.cache') : null, 96 | $name, 97 | $strictMode, 98 | new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE), 99 | ]; 100 | $definition = new Definition(EntrypointLookup::class, $arguments); 101 | $definition->addTag('kernel.reset', ['method' => 'reset']); 102 | $container->setDefinition($id, $definition); 103 | 104 | return new Reference($id); 105 | } 106 | 107 | private function getEntrypointServiceId(string $name): string 108 | { 109 | return \sprintf('webpack_encore.entrypoint_lookup[%s]', $name); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Event/RenderAssetTagEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\Event; 13 | 14 | /** 15 | * Dispatched each time a script or link tag is rendered. 16 | */ 17 | final class RenderAssetTagEvent 18 | { 19 | public const TYPE_SCRIPT = 'script'; 20 | public const TYPE_LINK = 'link'; 21 | 22 | private $type; 23 | private $url; 24 | private $attributes; 25 | 26 | public function __construct(string $type, string $url, array $attributes) 27 | { 28 | $this->type = $type; 29 | $this->url = $url; 30 | $this->attributes = $attributes; 31 | } 32 | 33 | public function isScriptTag(): bool 34 | { 35 | return self::TYPE_SCRIPT === $this->type; 36 | } 37 | 38 | public function isLinkTag(): bool 39 | { 40 | return self::TYPE_LINK === $this->type; 41 | } 42 | 43 | public function getUrl(): string 44 | { 45 | return $this->url; 46 | } 47 | 48 | public function getAttributes(): array 49 | { 50 | return $this->attributes; 51 | } 52 | 53 | /** 54 | * @param string $name The attribute name 55 | * @param string|bool $value Value can be "true" to have an attribute without a value (e.g. "defer") 56 | */ 57 | public function setAttribute(string $name, $value): void 58 | { 59 | $this->attributes[$name] = $value; 60 | } 61 | 62 | public function removeAttribute(string $name): void 63 | { 64 | unset($this->attributes[$name]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/EventListener/ExceptionListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\EventListener; 13 | 14 | use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollection; 15 | 16 | class ExceptionListener 17 | { 18 | private $entrypointLookupCollection; 19 | 20 | private $buildNames; 21 | 22 | public function __construct(EntrypointLookupCollection $entrypointLookupCollection, array $buildNames) 23 | { 24 | $this->entrypointLookupCollection = $entrypointLookupCollection; 25 | $this->buildNames = $buildNames; 26 | } 27 | 28 | public function onKernelException(): void 29 | { 30 | foreach ($this->buildNames as $buildName) { 31 | $this->entrypointLookupCollection->getEntrypointLookup($buildName)->reset(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/EventListener/PreLoadAssetsEventListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\EventListener; 13 | 14 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 15 | use Symfony\Component\HttpKernel\Event\ResponseEvent; 16 | use Symfony\Component\WebLink\GenericLinkProvider; 17 | use Symfony\Component\WebLink\Link; 18 | use Symfony\WebpackEncoreBundle\Asset\TagRenderer; 19 | 20 | /** 21 | * @author Ryan Weaver 22 | */ 23 | class PreLoadAssetsEventListener implements EventSubscriberInterface 24 | { 25 | private $tagRenderer; 26 | 27 | public function __construct(TagRenderer $tagRenderer) 28 | { 29 | $this->tagRenderer = $tagRenderer; 30 | } 31 | 32 | public function onKernelResponse(ResponseEvent $event): void 33 | { 34 | if (!$event->isMainRequest()) { 35 | return; 36 | } 37 | 38 | $request = $event->getRequest(); 39 | 40 | if (null === $linkProvider = $request->attributes->get('_links')) { 41 | $request->attributes->set( 42 | '_links', 43 | new GenericLinkProvider() 44 | ); 45 | } 46 | 47 | /** @var GenericLinkProvider $linkProvider */ 48 | $linkProvider = $request->attributes->get('_links'); 49 | $defaultAttributes = $this->tagRenderer->getDefaultAttributes(); 50 | 51 | foreach ($this->tagRenderer->getRenderedScripts(true) as $attributes) { 52 | $src = $attributes['src']; 53 | unset($attributes['src']); 54 | $attributes = [...$defaultAttributes, ...$attributes]; 55 | 56 | $link = $this->createLink('preload', $src) 57 | ->withAttribute('as', 'script'); 58 | 59 | foreach ($attributes as $k => $v) { 60 | $link = $link->withAttribute($k, $v); 61 | } 62 | 63 | $linkProvider = $linkProvider->withLink($link); 64 | } 65 | 66 | foreach ($this->tagRenderer->getRenderedStyles(true) as $attributes) { 67 | $href = $attributes['href']; 68 | unset($attributes['href']); 69 | $attributes = [...$defaultAttributes, ...$attributes]; 70 | 71 | $link = $this->createLink('preload', $href)->withAttribute('as', 'style'); 72 | 73 | foreach ($attributes as $k => $v) { 74 | $link = $link->withAttribute($k, $v); 75 | } 76 | 77 | $linkProvider = $linkProvider->withLink($link); 78 | } 79 | 80 | $request->attributes->set('_links', $linkProvider); 81 | } 82 | 83 | public static function getSubscribedEvents(): array 84 | { 85 | return [ 86 | // must run before AddLinkHeaderListener 87 | 'kernel.response' => ['onKernelResponse', 50], 88 | ]; 89 | } 90 | 91 | private function createLink(string $rel, string $href): Link 92 | { 93 | return new Link($rel, $href); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/EventListener/ResetAssetsEventListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Symfony\WebpackEncoreBundle\EventListener; 15 | 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Symfony\Component\HttpKernel\Event\FinishRequestEvent; 18 | use Symfony\Component\HttpKernel\KernelEvents; 19 | use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollection; 20 | 21 | class ResetAssetsEventListener implements EventSubscriberInterface 22 | { 23 | private $entrypointLookupCollection; 24 | private $buildNames; 25 | 26 | public function __construct(EntrypointLookupCollection $entrypointLookupCollection, array $buildNames) 27 | { 28 | $this->entrypointLookupCollection = $entrypointLookupCollection; 29 | $this->buildNames = $buildNames; 30 | } 31 | 32 | public static function getSubscribedEvents(): array 33 | { 34 | return [ 35 | KernelEvents::FINISH_REQUEST => 'resetAssets', 36 | ]; 37 | } 38 | 39 | public function resetAssets(FinishRequestEvent $event): void 40 | { 41 | if (!$event->isMainRequest()) { 42 | return; 43 | } 44 | foreach ($this->buildNames as $name) { 45 | $this->entrypointLookupCollection->getEntrypointLookup($name)->reset(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Exception/EntrypointNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\Exception; 13 | 14 | class EntrypointNotFoundException extends \InvalidArgumentException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/UndefinedBuildException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\Exception; 13 | 14 | class UndefinedBuildException extends \InvalidArgumentException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | %kernel.build_dir%/webpack_encore.cache.php 44 | 45 | 46 | 47 | 48 | %kernel.build_dir%/webpack_encore.cache.php 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/Twig/EntryFilesTwigExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle\Twig; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup; 16 | use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface; 17 | use Symfony\WebpackEncoreBundle\Asset\TagRenderer; 18 | use Twig\Extension\AbstractExtension; 19 | use Twig\TwigFunction; 20 | 21 | final class EntryFilesTwigExtension extends AbstractExtension 22 | { 23 | private $container; 24 | 25 | public function __construct(ContainerInterface $container) 26 | { 27 | $this->container = $container; 28 | } 29 | 30 | public function getFunctions(): array 31 | { 32 | return [ 33 | new TwigFunction('encore_entry_js_files', [$this, 'getWebpackJsFiles']), 34 | new TwigFunction('encore_entry_css_files', [$this, 'getWebpackCssFiles']), 35 | new TwigFunction('encore_entry_script_tags', [$this, 'renderWebpackScriptTags'], ['is_safe' => ['html']]), 36 | new TwigFunction('encore_entry_link_tags', [$this, 'renderWebpackLinkTags'], ['is_safe' => ['html']]), 37 | new TwigFunction('encore_entry_exists', [$this, 'entryExists']), 38 | ]; 39 | } 40 | 41 | public function getWebpackJsFiles(string $entryName, string $entrypointName = '_default'): array 42 | { 43 | return $this->getEntrypointLookup($entrypointName) 44 | ->getJavaScriptFiles($entryName); 45 | } 46 | 47 | public function getWebpackCssFiles(string $entryName, string $entrypointName = '_default'): array 48 | { 49 | return $this->getEntrypointLookup($entrypointName) 50 | ->getCssFiles($entryName); 51 | } 52 | 53 | public function renderWebpackScriptTags(string $entryName, ?string $packageName = null, string $entrypointName = '_default', array $attributes = []): string 54 | { 55 | return $this->getTagRenderer() 56 | ->renderWebpackScriptTags($entryName, $packageName, $entrypointName, $attributes); 57 | } 58 | 59 | public function renderWebpackLinkTags(string $entryName, ?string $packageName = null, string $entrypointName = '_default', array $attributes = []): string 60 | { 61 | return $this->getTagRenderer() 62 | ->renderWebpackLinkTags($entryName, $packageName, $entrypointName, $attributes); 63 | } 64 | 65 | public function entryExists(string $entryName, string $entrypointName = '_default'): bool 66 | { 67 | $entrypointLookup = $this->getEntrypointLookup($entrypointName); 68 | if (!$entrypointLookup instanceof EntrypointLookup) { 69 | throw new \LogicException(\sprintf('Cannot use entryExists() unless the entrypoint lookup is an instance of "%s"', EntrypointLookup::class)); 70 | } 71 | 72 | return $entrypointLookup->entryExists($entryName); 73 | } 74 | 75 | private function getEntrypointLookup(string $entrypointName): EntrypointLookupInterface 76 | { 77 | return $this->container->get('webpack_encore.entrypoint_lookup_collection') 78 | ->getEntrypointLookup($entrypointName); 79 | } 80 | 81 | private function getTagRenderer(): TagRenderer 82 | { 83 | return $this->container->get('webpack_encore.tag_renderer'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/WebpackEncoreBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\WebpackEncoreBundle; 13 | 14 | use Symfony\Component\HttpKernel\Bundle\Bundle; 15 | 16 | final class WebpackEncoreBundle extends Bundle 17 | { 18 | } 19 | --------------------------------------------------------------------------------