├── .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 |
--------------------------------------------------------------------------------