Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new functions to disable "file tracking" & ease inline_css usage #91

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 24 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,46 +101,27 @@ and `build/entry1.js`, then `encore_entry_script_tags()` is equivalent to:
<script src="{{ asset('build/entry1.js') }}"></script>
```

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)

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.

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.

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:

```php
// src/Controller/SomeController.php

use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;

class SomeController
{
public function index(EntrypointLookupInterface $entrypointLookup)
{
$entrypointLookup->reset();
// render a template

$entrypointLookup->reset();
// render another template

// ...
}
}
```

If you have multiple builds, you can also autowire
`Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollectionInterface`
and use it to get the `EntrypointLookupInterface` object for any build.
## Twig Function Reference

* `encore_entry_script_tags('entryName')` - renders all `<script>` tags for
the entry.
* `encore_entry_link_tags('entryName')` - renders all `<link` tags for the
entry.
* `encore_entry_js_files('entryName')` - returns an array of all JavaScript
files needed for the entry.
* `encore_entry_css_files('entryName')` - returns an array of all CSS
files needed for the entry.
* `encore_entry_js_source('entryName')` - returns the full source from all
the JavaScript files needed for this entry.
* `encore_entry_css_source('entryName')` - returns the full source from all
the CSS files needed for this entry.
* `encore_disable_file_tracking()` - tells Twig to stop "file tracking" and
render *all* CSS/JS files, even if they were previously rendered.
* `encore_enable_file_tracking()` - tells Twig to re-enable "file tracking" and
render *only* CSS/JS files tht were not previously rendered.

You can also manually "reset" file tracking in PHP by autowiring
the `Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface` service
and calling `$entrypointLookup->reset()`. If you're using multiple builds,
you can autowire `Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollectionInterface`
and then ask for your the correct `EntrypointLookupInterface`.
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;
}
}
27 changes: 26 additions & 1 deletion src/Asset/EntrypointLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class EntrypointLookup implements EntrypointLookupInterface, IntegrityDataProvid

private $strictMode;

private $trackReturnedFiles = true;

public function __construct(string $entrypointJsonPath, CacheItemPoolInterface $cache = null, string $cacheKey = null, bool $strictMode = true)
{
$this->entrypointJsonPath = $entrypointJsonPath;
Expand Down Expand Up @@ -70,6 +72,23 @@ public function reset()
$this->returnedFiles = [];
}

/**
* Can be used to disable file tracking.
*
* When file tracking is disabled, *all* CSS and JS files will be
* returned from getJavaScriptFiles() and getCssFiles() including
* those that were previously returned.
*/
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 All @@ -81,8 +100,14 @@ private function getEntryFiles(string $entryName, string $key): array
return [];
}

// make sure to not return the same file multiple times
$entryFiles = $entryData[$key];

// if tracking is disabled, return everything & do not mutate returnedFiles list
if (!$this->trackReturnedFiles) {
return $entryFiles;
}

// make sure to not return the same file multiple times
$newFiles = array_values(array_diff($entryFiles, $this->returnedFiles));
$this->returnedFiles = array_merge($this->returnedFiles, $newFiles);

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>
82 changes: 82 additions & 0 deletions src/Twig/EntryFilesTwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
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;
use Twig\Extension\AbstractExtension;
Expand All @@ -29,8 +31,12 @@ 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']),
new TwigFunction('encore_enable_file_tracking', [$this, 'enableReturnedFileTracking']),
];
}

Expand All @@ -46,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 All @@ -58,6 +92,49 @@ public function renderWebpackLinkTags(string $entryName, string $packageName = n
->renderWebpackLinkTags($entryName, $packageName, $entrypointName);
}

public function disableReturnedFileTracking(string $entrypointName = '_default')
{
$this->changeReturnedFileTracking(false, $entrypointName);
}

public function enableReturnedFileTracking(string $entrypointName = '_default')
{
$this->changeReturnedFileTracking(true, $entrypointName);
}

private function changeReturnedFileTracking(bool $isEnabled, string $entrypointName)
{
$lookup = $this->getEntrypointLookup($entrypointName);

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

$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 @@ -68,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');
}
}