Skip to content

Commit

Permalink
Adding new Twig functions to fetch the *source* of files
Browse files Browse the repository at this point in the history
  • Loading branch information
weaverryan committed May 8, 2020
1 parent b8c983e commit 3287980
Show file tree
Hide file tree
Showing 15 changed files with 352 additions and 11 deletions.
64 changes: 55 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,67 @@ If you want more control, you can use the `encore_entry_js_files()` and
`encore_entry_css_files()` methods to get the list of files needed, then
loop and create the `script` and `link` tags manually.

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

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

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

In that case, before each render, you'll need to "reset" the internal
cache so that the bundle re-renders CSS or JS files that it previously
rendered. For example, in a controller:
The easiest solution is to render the raw CSS and JavaScript using
a special function that *always* returns the full source, even for files
that were already rendered.

This works especially well in emails thanks to the
[inline_css](https://github.com/twigphp/cssinliner-extra) filter:

```twig
{% apply inline_css(encore_entry_css_source('my_entry')) %}
<div>
Hi! The CSS from my_entry will be converted into
inline styles on any HTML elements inside.
</div>
{% endapply %}
```

Or you can just render the source directly.

```twig
<style>
{{ encore_entry_css_source('my_entry')|raw }}
</style>
<script>
{{ encore_entry_js_source('my_entry')|raw }}
</script>
```

If you can't use these `encore_entry_*_source` functions, you can instead
manually disable and enable "file tracking":

```twig
{# some template that renders a PDF or an email #}
{% do encore_disable_file_tracking() %}
{{ encore_entry_link_tags('entry1') }}
{{ encore_entry_script_tags('entry1') }}
{% do encore_enable_file_tracking() %}
```

With this, *all* JS and CSS files for `entry1` will be rendered and
this won't affect any other Twig templates rendered in the request.

## Resetting the Entrypoint

If using `encore_disable_file_tracking()` won't work for you for some
reason, you can also "reset" EncoreBundle's internal cache so that the
bundle re-renders CSS or JS files that it previously rendered. For
example, in a controller:

```php
// src/Controller/SomeController.php
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0",
"symfony/phpunit-bridge": "^4.3.5 || ^5.0",
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0",
"symfony/web-link": "^3.4 || ^4.0 || ^5.0"
"symfony/web-link": "^3.4 || ^4.0 || ^5.0",
"twig/extra-bundle": "3.x-dev",
"twig/cssinliner-extra": "3.x-dev"
},
"extra": {
"thanks": {
Expand Down
96 changes: 96 additions & 0 deletions src/Asset/BuildFileLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Symfony\WebpackEncoreBundle\Asset;

/**
* Attempts to read the source of built files.
*
* @internal
*/
final class BuildFileLocator
{
private $buildPaths;

private $ensureFileExists = true;

/**
* @param string[] $buildPaths
*/
public function __construct(array $buildPaths)
{
$this->buildPaths = $buildPaths;
}

public function findFile(string $path, string $buildName = '_default'): string
{
if (!isset($this->buildPaths[$buildName])) {
throw new \InvalidArgumentException(sprintf('Invalid build name "%s"', $buildName));
}

// sanity / security check
if (!$this->strEndsWith($path, '.css') && !$this->strEndsWith($path, '.js')) {
throw new \InvalidArgumentException('Can only read files ending in .css and .js');
}

$buildPath = $this->buildPaths[$buildName];

$targetPath = $this->combinePaths($buildPath, $path);

if ($this->ensureFileExists && !file_exists($targetPath)) {
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));
}

return $targetPath;
}

/**
* This method tries to combine the build path and asset path to get a final path.
*
* It's really an "attempt" and will work in all normal cases, but not
* in all cases. For example with this config:
*
* output_path: %kernel.project_dir%/public/build
*
* If you pass an asset whose path is "build/file1.js", this would
* remove the duplicated "build" on both and return a final path of:
*
* %kernel.project_dir%/public/build/file1.js
*/
private function combinePaths(string $buildPath, string $path): string
{
$pathParts = explode('/', ltrim($path, '/'));
$buildPathInfo = new \SplFileInfo($buildPath);

while (true) {
// break if there are no directories left
if (count($pathParts) == 1) {
break;
}

// break if the beginning of the path and the "directory name" of the build path
// don't intersect
if ($pathParts[0] !== $buildPathInfo->getFilename()) {
break;
}

// pop the first "directory" off of the path
unset($pathParts[0]);
$pathParts = array_values($pathParts);
}

return $buildPathInfo->getPathname().'/'.implode('/', $pathParts);
}

private function strEndsWith(string $haystack, string $needle): bool
{
return '' === $needle || $needle === \substr($haystack, -\strlen($needle));
}

/**
* @internal
*/
public function disableFileExistsCheck(): void
{
$this->ensureFileExists = false;
}
}
5 changes: 5 additions & 0 deletions src/Asset/EntrypointLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public function enableReturnedFileTracking(bool $shouldTrackReturnedFiles)
$this->trackReturnedFiles = $shouldTrackReturnedFiles;
}

public function isReturnedFileTrackingEnabled(): bool
{
return $this->trackReturnedFiles;
}

private function getEntryFiles(string $entryName, string $key): array
{
$this->validateEntryName($entryName);
Expand Down
6 changes: 6 additions & 0 deletions src/DependencyInjection/WebpackEncoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ public function load(array $configs, ContainerBuilder $container)

$factories = [];
$cacheKeys = [];
$buildPaths = [];

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

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

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

$container->getDefinition('webpack_encore.build_file_locator')
->replaceArgument(0, $buildPaths);

$defaultAttributes = [];

if (false !== $config['crossorigin']) {
Expand Down
5 changes: 5 additions & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<argument type="collection">
<argument key="webpack_encore.entrypoint_lookup_collection" type="service" id="webpack_encore.entrypoint_lookup_collection" />
<argument key="webpack_encore.tag_renderer" type="service" id="webpack_encore.tag_renderer" />
<argument key="webpack_encore.build_file_locator" type="service" id="webpack_encore.build_file_locator" />
</argument>
</service>
</argument>
Expand Down Expand Up @@ -60,5 +61,9 @@
<tag name="kernel.event_subscriber" />
<argument type="service" id="webpack_encore.tag_renderer" />
</service>

<service id="webpack_encore.build_file_locator" class="Symfony\WebpackEncoreBundle\Asset\BuildFileLocator">
<argument /> <!-- build paths -->
</service>
</services>
</container>
58 changes: 58 additions & 0 deletions src/Twig/EntryFilesTwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Symfony\WebpackEncoreBundle\Twig;

use Psr\Container\ContainerInterface;
use Symfony\WebpackEncoreBundle\Asset\BuildFileLocator;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookup;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
Expand All @@ -30,6 +31,8 @@ public function getFunctions()
return [
new TwigFunction('encore_entry_js_files', [$this, 'getWebpackJsFiles']),
new TwigFunction('encore_entry_css_files', [$this, 'getWebpackCssFiles']),
new TwigFunction('encore_entry_js_source', [$this, 'getWebpackJsSource']),
new TwigFunction('encore_entry_css_source', [$this, 'getWebpackCssSource']),
new TwigFunction('encore_entry_script_tags', [$this, 'renderWebpackScriptTags'], ['is_safe' => ['html']]),
new TwigFunction('encore_entry_link_tags', [$this, 'renderWebpackLinkTags'], ['is_safe' => ['html']]),
new TwigFunction('encore_disable_file_tracking', [$this, 'disableReturnedFileTracking']),
Expand All @@ -49,6 +52,34 @@ public function getWebpackCssFiles(string $entryName, string $entrypointName = '
->getCssFiles($entryName);
}

public function getWebpackJsSource(string $entryName, string $entrypointName = '_default'): string
{
$originalTrackingValue = $this->isReturnedFileTrackingEnabled($entrypointName);
$this->changeReturnedFileTracking(false, $entrypointName);

$files = $this->getEntrypointLookup($entrypointName)
->getJavaScriptFiles($entryName);

$source = $this->concatenateFileSources($files);
$this->changeReturnedFileTracking($originalTrackingValue, $entrypointName);

return $source;
}

public function getWebpackCssSource(string $entryName, string $entrypointName = '_default'): string
{
$originalTrackingValue = $this->isReturnedFileTrackingEnabled($entrypointName);
$this->changeReturnedFileTracking(false, $entrypointName);

$files = $this->getEntrypointLookup($entrypointName)
->getCssFiles($entryName);

$source = $this->concatenateFileSources($files);
$this->changeReturnedFileTracking($originalTrackingValue, $entrypointName);

return $source;
}

public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
{
return $this->getTagRenderer()
Expand Down Expand Up @@ -82,6 +113,28 @@ private function changeReturnedFileTracking(bool $isEnabled, string $entrypointN
$lookup->enableReturnedFileTracking($isEnabled);
}

private function isReturnedFileTrackingEnabled(string $entrypointName): bool
{
$lookup = $this->getEntrypointLookup($entrypointName);

if (!$lookup instanceof EntrypointLookup) {
throw new \LogicException('In order to use encore_entry_js_source/encore_entry_css_source, the EntrypointLookupInterface must be an instance of EntrypointLookup.');
}

return $lookup->isReturnedFileTrackingEnabled();
}

private function concatenateFileSources(array $files): string
{
$locator = $this->getBuildFileLocator();
$source = '';
foreach ($files as $file) {
$source .= file_get_contents($locator->findFile($file));
}

return $source;
}

private function getEntrypointLookup(string $entrypointName): EntrypointLookupInterface
{
return $this->container->get('webpack_encore.entrypoint_lookup_collection')
Expand All @@ -92,4 +145,9 @@ private function getTagRenderer(): TagRenderer
{
return $this->container->get('webpack_encore.tag_renderer');
}

private function getBuildFileLocator(): BuildFileLocator
{
return $this->container->get('webpack_encore.build_file_locator');
}
}

0 comments on commit 3287980

Please sign in to comment.