Skip to content

Commit 3287980

Browse files
committed
Adding new Twig functions to fetch the *source* of files
1 parent b8c983e commit 3287980

15 files changed

+352
-11
lines changed

README.md

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,67 @@ If you want more control, you can use the `encore_entry_js_files()` and
105105
`encore_entry_css_files()` methods to get the list of files needed, then
106106
loop and create the `script` and `link` tags manually.
107107

108-
## Rendering Multiple Times in a Request (e.g. to Generate a PDF)
108+
## Rendering Multiple Templates (e.g. Emails or PDFs)
109109

110110
When you render your script or link tags, the bundle is smart enough
111111
not to repeat the same JavaScript or CSS file within the same request.
112112
This prevents you from having duplicate `<link>` or `<script>` tags
113-
if you render multiple entries that both rely on the same file.
113+
if you render multiple entries that rely on the same file.
114114

115-
In some cases, however, you may want to render the script & link
116-
tags for the same entry multiple times in a request. For example,
117-
if you render multiple Twig templates to create multiple PDF files
118-
during a single request.
115+
But if you're purposely rendering multiple templates in the same
116+
request - e.g. rendering a template for a PDF or to send an email -
117+
then this can cause problems: the later templates won't include any
118+
`<link>` or `<script>` tags that were rendered in an earlier template.
119119

120-
In that case, before each render, you'll need to "reset" the internal
121-
cache so that the bundle re-renders CSS or JS files that it previously
122-
rendered. For example, in a controller:
120+
The easiest solution is to render the raw CSS and JavaScript using
121+
a special function that *always* returns the full source, even for files
122+
that were already rendered.
123+
124+
This works especially well in emails thanks to the
125+
[inline_css](https://github.com/twigphp/cssinliner-extra) filter:
126+
127+
```twig
128+
{% apply inline_css(encore_entry_css_source('my_entry')) %}
129+
<div>
130+
Hi! The CSS from my_entry will be converted into
131+
inline styles on any HTML elements inside.
132+
</div>
133+
{% endapply %}
134+
```
135+
136+
Or you can just render the source directly.
137+
138+
```twig
139+
<style>
140+
{{ encore_entry_css_source('my_entry')|raw }}
141+
</style>
142+
143+
<script>
144+
{{ encore_entry_js_source('my_entry')|raw }}
145+
</script>
146+
```
147+
148+
If you can't use these `encore_entry_*_source` functions, you can instead
149+
manually disable and enable "file tracking":
150+
151+
```twig
152+
{# some template that renders a PDF or an email #}
153+
154+
{% do encore_disable_file_tracking() %}
155+
{{ encore_entry_link_tags('entry1') }}
156+
{{ encore_entry_script_tags('entry1') }}
157+
{% do encore_enable_file_tracking() %}
158+
```
159+
160+
With this, *all* JS and CSS files for `entry1` will be rendered and
161+
this won't affect any other Twig templates rendered in the request.
162+
163+
## Resetting the Entrypoint
164+
165+
If using `encore_disable_file_tracking()` won't work for you for some
166+
reason, you can also "reset" EncoreBundle's internal cache so that the
167+
bundle re-renders CSS or JS files that it previously rendered. For
168+
example, in a controller:
123169

124170
```php
125171
// src/Controller/SomeController.php

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0",
3333
"symfony/phpunit-bridge": "^4.3.5 || ^5.0",
3434
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0",
35-
"symfony/web-link": "^3.4 || ^4.0 || ^5.0"
35+
"symfony/web-link": "^3.4 || ^4.0 || ^5.0",
36+
"twig/extra-bundle": "3.x-dev",
37+
"twig/cssinliner-extra": "3.x-dev"
3638
},
3739
"extra": {
3840
"thanks": {

src/Asset/BuildFileLocator.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace Symfony\WebpackEncoreBundle\Asset;
4+
5+
/**
6+
* Attempts to read the source of built files.
7+
*
8+
* @internal
9+
*/
10+
final class BuildFileLocator
11+
{
12+
private $buildPaths;
13+
14+
private $ensureFileExists = true;
15+
16+
/**
17+
* @param string[] $buildPaths
18+
*/
19+
public function __construct(array $buildPaths)
20+
{
21+
$this->buildPaths = $buildPaths;
22+
}
23+
24+
public function findFile(string $path, string $buildName = '_default'): string
25+
{
26+
if (!isset($this->buildPaths[$buildName])) {
27+
throw new \InvalidArgumentException(sprintf('Invalid build name "%s"', $buildName));
28+
}
29+
30+
// sanity / security check
31+
if (!$this->strEndsWith($path, '.css') && !$this->strEndsWith($path, '.js')) {
32+
throw new \InvalidArgumentException('Can only read files ending in .css and .js');
33+
}
34+
35+
$buildPath = $this->buildPaths[$buildName];
36+
37+
$targetPath = $this->combinePaths($buildPath, $path);
38+
39+
if ($this->ensureFileExists && !file_exists($targetPath)) {
40+
throw new \LogicException(sprintf('Cannot determine how to locate the "%s" file by combining with the output_path "%s". Looked in "%s".', $path, $buildPath, $targetPath));
41+
}
42+
43+
return $targetPath;
44+
}
45+
46+
/**
47+
* This method tries to combine the build path and asset path to get a final path.
48+
*
49+
* It's really an "attempt" and will work in all normal cases, but not
50+
* in all cases. For example with this config:
51+
*
52+
* output_path: %kernel.project_dir%/public/build
53+
*
54+
* If you pass an asset whose path is "build/file1.js", this would
55+
* remove the duplicated "build" on both and return a final path of:
56+
*
57+
* %kernel.project_dir%/public/build/file1.js
58+
*/
59+
private function combinePaths(string $buildPath, string $path): string
60+
{
61+
$pathParts = explode('/', ltrim($path, '/'));
62+
$buildPathInfo = new \SplFileInfo($buildPath);
63+
64+
while (true) {
65+
// break if there are no directories left
66+
if (count($pathParts) == 1) {
67+
break;
68+
}
69+
70+
// break if the beginning of the path and the "directory name" of the build path
71+
// don't intersect
72+
if ($pathParts[0] !== $buildPathInfo->getFilename()) {
73+
break;
74+
}
75+
76+
// pop the first "directory" off of the path
77+
unset($pathParts[0]);
78+
$pathParts = array_values($pathParts);
79+
}
80+
81+
return $buildPathInfo->getPathname().'/'.implode('/', $pathParts);
82+
}
83+
84+
private function strEndsWith(string $haystack, string $needle): bool
85+
{
86+
return '' === $needle || $needle === \substr($haystack, -\strlen($needle));
87+
}
88+
89+
/**
90+
* @internal
91+
*/
92+
public function disableFileExistsCheck(): void
93+
{
94+
$this->ensureFileExists = false;
95+
}
96+
}

src/Asset/EntrypointLookup.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ public function enableReturnedFileTracking(bool $shouldTrackReturnedFiles)
8484
$this->trackReturnedFiles = $shouldTrackReturnedFiles;
8585
}
8686

87+
public function isReturnedFileTrackingEnabled(): bool
88+
{
89+
return $this->trackReturnedFiles;
90+
}
91+
8792
private function getEntryFiles(string $entryName, string $key): array
8893
{
8994
$this->validateEntryName($entryName);

src/DependencyInjection/WebpackEncoreExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ public function load(array $configs, ContainerBuilder $container)
3535

3636
$factories = [];
3737
$cacheKeys = [];
38+
$buildPaths = [];
3839

3940
if (false !== $config['output_path']) {
4041
$factories['_default'] = $this->entrypointFactory($container, '_default', $config['output_path'], $config['cache'], $config['strict_mode']);
4142
$cacheKeys['_default'] = $config['output_path'].'/'.self::ENTRYPOINTS_FILE_NAME;
43+
$buildPaths['_default'] = $config['output_path'];
4244

4345
$container->getDefinition('webpack_encore.entrypoint_lookup_collection')
4446
->setArgument(1, '_default');
@@ -47,6 +49,7 @@ public function load(array $configs, ContainerBuilder $container)
4749
foreach ($config['builds'] as $name => $path) {
4850
$factories[$name] = $this->entrypointFactory($container, $name, $path, $config['cache'], $config['strict_mode']);
4951
$cacheKeys[rawurlencode($name)] = $path.'/'.self::ENTRYPOINTS_FILE_NAME;
52+
$buildPaths[$name] = $path;
5053
}
5154

5255
$container->getDefinition('webpack_encore.exception_listener')
@@ -61,6 +64,9 @@ public function load(array $configs, ContainerBuilder $container)
6164
$container->setAlias(EntrypointLookupInterface::class, new Alias($this->getEntrypointServiceId('_default')));
6265
}
6366

67+
$container->getDefinition('webpack_encore.build_file_locator')
68+
->replaceArgument(0, $buildPaths);
69+
6470
$defaultAttributes = [];
6571

6672
if (false !== $config['crossorigin']) {

src/Resources/config/services.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<argument type="collection">
2929
<argument key="webpack_encore.entrypoint_lookup_collection" type="service" id="webpack_encore.entrypoint_lookup_collection" />
3030
<argument key="webpack_encore.tag_renderer" type="service" id="webpack_encore.tag_renderer" />
31+
<argument key="webpack_encore.build_file_locator" type="service" id="webpack_encore.build_file_locator" />
3132
</argument>
3233
</service>
3334
</argument>
@@ -60,5 +61,9 @@
6061
<tag name="kernel.event_subscriber" />
6162
<argument type="service" id="webpack_encore.tag_renderer" />
6263
</service>
64+
65+
<service id="webpack_encore.build_file_locator" class="Symfony\WebpackEncoreBundle\Asset\BuildFileLocator">
66+
<argument /> <!-- build paths -->
67+
</service>
6368
</services>
6469
</container>

src/Twig/EntryFilesTwigExtension.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace Symfony\WebpackEncoreBundle\Twig;
1111

1212
use Psr\Container\ContainerInterface;
13+
use Symfony\WebpackEncoreBundle\Asset\BuildFileLocator;
1314
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup;
1415
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
1516
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
@@ -30,6 +31,8 @@ public function getFunctions()
3031
return [
3132
new TwigFunction('encore_entry_js_files', [$this, 'getWebpackJsFiles']),
3233
new TwigFunction('encore_entry_css_files', [$this, 'getWebpackCssFiles']),
34+
new TwigFunction('encore_entry_js_source', [$this, 'getWebpackJsSource']),
35+
new TwigFunction('encore_entry_css_source', [$this, 'getWebpackCssSource']),
3336
new TwigFunction('encore_entry_script_tags', [$this, 'renderWebpackScriptTags'], ['is_safe' => ['html']]),
3437
new TwigFunction('encore_entry_link_tags', [$this, 'renderWebpackLinkTags'], ['is_safe' => ['html']]),
3538
new TwigFunction('encore_disable_file_tracking', [$this, 'disableReturnedFileTracking']),
@@ -49,6 +52,34 @@ public function getWebpackCssFiles(string $entryName, string $entrypointName = '
4952
->getCssFiles($entryName);
5053
}
5154

55+
public function getWebpackJsSource(string $entryName, string $entrypointName = '_default'): string
56+
{
57+
$originalTrackingValue = $this->isReturnedFileTrackingEnabled($entrypointName);
58+
$this->changeReturnedFileTracking(false, $entrypointName);
59+
60+
$files = $this->getEntrypointLookup($entrypointName)
61+
->getJavaScriptFiles($entryName);
62+
63+
$source = $this->concatenateFileSources($files);
64+
$this->changeReturnedFileTracking($originalTrackingValue, $entrypointName);
65+
66+
return $source;
67+
}
68+
69+
public function getWebpackCssSource(string $entryName, string $entrypointName = '_default'): string
70+
{
71+
$originalTrackingValue = $this->isReturnedFileTrackingEnabled($entrypointName);
72+
$this->changeReturnedFileTracking(false, $entrypointName);
73+
74+
$files = $this->getEntrypointLookup($entrypointName)
75+
->getCssFiles($entryName);
76+
77+
$source = $this->concatenateFileSources($files);
78+
$this->changeReturnedFileTracking($originalTrackingValue, $entrypointName);
79+
80+
return $source;
81+
}
82+
5283
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
5384
{
5485
return $this->getTagRenderer()
@@ -82,6 +113,28 @@ private function changeReturnedFileTracking(bool $isEnabled, string $entrypointN
82113
$lookup->enableReturnedFileTracking($isEnabled);
83114
}
84115

116+
private function isReturnedFileTrackingEnabled(string $entrypointName): bool
117+
{
118+
$lookup = $this->getEntrypointLookup($entrypointName);
119+
120+
if (!$lookup instanceof EntrypointLookup) {
121+
throw new \LogicException('In order to use encore_entry_js_source/encore_entry_css_source, the EntrypointLookupInterface must be an instance of EntrypointLookup.');
122+
}
123+
124+
return $lookup->isReturnedFileTrackingEnabled();
125+
}
126+
127+
private function concatenateFileSources(array $files): string
128+
{
129+
$locator = $this->getBuildFileLocator();
130+
$source = '';
131+
foreach ($files as $file) {
132+
$source .= file_get_contents($locator->findFile($file));
133+
}
134+
135+
return $source;
136+
}
137+
85138
private function getEntrypointLookup(string $entrypointName): EntrypointLookupInterface
86139
{
87140
return $this->container->get('webpack_encore.entrypoint_lookup_collection')
@@ -92,4 +145,9 @@ private function getTagRenderer(): TagRenderer
92145
{
93146
return $this->container->get('webpack_encore.tag_renderer');
94147
}
148+
149+
private function getBuildFileLocator(): BuildFileLocator
150+
{
151+
return $this->container->get('webpack_encore.build_file_locator');
152+
}
95153
}

0 commit comments

Comments
 (0)