diff --git a/.travis.yml b/.travis.yml
index e003ff4b..0528ff42 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,7 +20,7 @@ matrix:
env: deps=low
- php: 7.4
env: SYMFONY_PHPUNIT_VERSION=9.4
- - php: nightly
+ - php: 8.0
env: SYMFONY_PHPUNIT_VERSION=9.4
before_install:
diff --git a/README.md b/README.md
index 503be84c..5fb4c861 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,13 @@ webpack_encore:
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
+
+ # Set attributes that will be rendered on all script and link tags
+ # script_attributes:
+ # defer: true
+ # referrerpolicy: origin
+ # link_attributes:
+ # referrerpolicy: origin
# if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
@@ -84,6 +91,13 @@ For example, to render all of the `script` and `link` tags for a specific
{{ parent() }}
{{ encore_entry_script_tags('entry1') }}
+
+ {# or render a custom attribute #}
+ {#
+ {{ encore_entry_script_tags('entry1', attributes={
+ defer: true
+ }) }}
+ #}
{% endblock %}
{% block stylesheets %}
@@ -144,3 +158,44 @@ class SomeController
If you have multiple builds, you can also autowire
`Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollectionInterface`
and use it to get the `EntrypointLookupInterface` object for any build.
+
+## Custom Attributes on script and link Tags
+
+Custom attributes can be added to rendered `script` or `link` in 3
+different ways:
+
+1. Via global config (`script_attributes` and `link_attributes`) - see the
+ config example above.
+
+1. When rendering in Twig - see the `attributes` option in the docs above.
+
+1. By listening to the `Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent`
+ event. For example:
+
+```php
+ 'onRenderAssetTag'
+ ];
+ }
+
+ public function onRenderAssetTag(RenderAssetTagEvent $event)
+ {
+ if ($event->isScriptTag()) {
+ $event->setAttribute('nonce', 'lookup nonce');
+ }
+ }
+}
+```
+
+Ok, have fun!
diff --git a/composer.json b/composer.json
index e6fab536..896cf8e7 100644
--- a/composer.json
+++ b/composer.json
@@ -22,17 +22,17 @@
"minimum-stability": "dev",
"require": {
"php": ">=7.1.3",
- "symfony/asset": "^3.4 || ^4.0 || ^5.0",
- "symfony/config": "^3.4 || ^4.0 || ^5.0",
- "symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0",
- "symfony/http-kernel": "^3.4 || ^4.0 || ^5.0",
+ "symfony/asset": "^4.4 || ^5.0",
+ "symfony/config": "^4.4 || ^5.0",
+ "symfony/dependency-injection": "^4.4 || ^5.0",
+ "symfony/http-kernel": "^4.4 || ^5.0",
"symfony/service-contracts": "^1.0 || ^2.0"
},
"require-dev": {
- "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/framework-bundle": "^4.4 || ^5.0",
+ "symfony/phpunit-bridge": "^4.4 || ^5.0",
+ "symfony/twig-bundle": "^4.4 || ^5.0",
+ "symfony/web-link": "^4.4 || ^5.0"
},
"extra": {
"thanks": {
diff --git a/src/Asset/TagRenderer.php b/src/Asset/TagRenderer.php
index 4e5ffc25..ee0e72e7 100644
--- a/src/Asset/TagRenderer.php
+++ b/src/Asset/TagRenderer.php
@@ -11,7 +11,9 @@
use Symfony\Component\Asset\Packages;
use Symfony\Component\DependencyInjection\ServiceLocator;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Service\ResetInterface;
+use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
/**
* @final
@@ -19,17 +21,21 @@
class TagRenderer implements ResetInterface
{
private $entrypointLookupCollection;
-
private $packages;
-
private $defaultAttributes;
+ private $defaultScriptAttributes;
+ private $defaultLinkAttributes;
+ private $eventDispatcher;
private $renderedFiles = [];
public function __construct(
$entrypointLookupCollection,
Packages $packages,
- array $defaultAttributes = []
+ array $defaultAttributes = [],
+ array $defaultScriptAttributes = [],
+ array $defaultLinkAttributes = [],
+ EventDispatcherInterface $eventDispatcher = null
) {
if ($entrypointLookupCollection instanceof EntrypointLookupInterface) {
@trigger_error(sprintf('The "$entrypointLookupCollection" argument in method "%s()" must be an instance of EntrypointLookupCollection.', __METHOD__), E_USER_DEPRECATED);
@@ -47,24 +53,39 @@ public function __construct(
$this->packages = $packages;
$this->defaultAttributes = $defaultAttributes;
+ $this->defaultScriptAttributes = $defaultScriptAttributes;
+ $this->defaultLinkAttributes = $defaultLinkAttributes;
+ $this->eventDispatcher = $eventDispatcher;
$this->reset();
}
- public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
+ public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = null, array $extraAttributes = []): string
{
+ $entrypointName = $entrypointName ?: '_default';
$scriptTags = [];
$entryPointLookup = $this->getEntrypointLookup($entrypointName);
$integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
foreach ($entryPointLookup->getJavaScriptFiles($entryName) as $filename) {
- $attributes = $this->defaultAttributes;
+ $attributes = [];
$attributes['src'] = $this->getAssetPath($filename, $packageName);
+ $attributes = array_merge($attributes, $this->defaultAttributes, $this->defaultScriptAttributes, $extraAttributes);
if (isset($integrityHashes[$filename])) {
$attributes['integrity'] = $integrityHashes[$filename];
}
+ $event = new RenderAssetTagEvent(
+ RenderAssetTagEvent::TYPE_SCRIPT,
+ $attributes['src'],
+ $attributes
+ );
+ if (null !== $this->eventDispatcher) {
+ $event = $this->eventDispatcher->dispatch($event);
+ }
+ $attributes = $event->getAttributes();
+
$scriptTags[] = sprintf(
'',
$this->convertArrayToAttributes($attributes)
@@ -76,21 +97,33 @@ public function renderWebpackScriptTags(string $entryName, string $packageName =
return implode('', $scriptTags);
}
- public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
+ public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = null, array $extraAttributes = []): string
{
+ $entrypointName = $entrypointName ?: '_default';
$scriptTags = [];
$entryPointLookup = $this->getEntrypointLookup($entrypointName);
$integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
foreach ($entryPointLookup->getCssFiles($entryName) as $filename) {
- $attributes = $this->defaultAttributes;
+ $attributes = [];
$attributes['rel'] = 'stylesheet';
$attributes['href'] = $this->getAssetPath($filename, $packageName);
+ $attributes = array_merge($attributes, $this->defaultAttributes, $this->defaultLinkAttributes, $extraAttributes);
if (isset($integrityHashes[$filename])) {
$attributes['integrity'] = $integrityHashes[$filename];
}
+ $event = new RenderAssetTagEvent(
+ RenderAssetTagEvent::TYPE_LINK,
+ $attributes['href'],
+ $attributes
+ );
+ if (null !== $this->eventDispatcher) {
+ $this->eventDispatcher->dispatch($event);
+ }
+ $attributes = $event->getAttributes();
+
$scriptTags[] = sprintf(
'',
$this->convertArrayToAttributes($attributes)
@@ -146,6 +179,10 @@ private function convertArrayToAttributes(array $attributesMap): string
{
return implode(' ', array_map(
function ($key, $value) {
+ // allows for things like defer: true to only render "defer"
+ if ($value === true) {
+ return $key;
+ }
return sprintf('%s="%s"', $key, htmlentities($value));
},
array_keys($attributesMap),
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 9dce21d2..87b5ef1a 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -55,7 +55,7 @@ public function getConfigTreeBuilder()
->useAttributeAsKey('name')
->normalizeKeys(false)
->scalarPrototype()
- ->validate()
+ ->validate()
->always(function ($values) {
if (isset($values['_default'])) {
throw new InvalidDefinitionException("Key '_default' can't be used as build name.");
@@ -63,8 +63,23 @@ public function getConfigTreeBuilder()
return $values;
})
+ ->end()
->end()
->end()
+ ->arrayNode('script_attributes')
+ ->info('Key/value pair of attributes to render on all script tags')
+ ->example('{ defer: true, referrerpolicy: "origin" }')
+ ->useAttributeAsKey('name')
+ ->normalizeKeys(false)
+ ->scalarPrototype()->end()
+ ->end()
+ ->arrayNode('link_attributes')
+ ->info('Key/value pair of attributes to render on all CSS link tags')
+ ->example('{ referrerpolicy: "origin" }')
+ ->useAttributeAsKey('name')
+ ->normalizeKeys(false)
+ ->scalarPrototype()->end()
+ ->end()
->end()
;
diff --git a/src/DependencyInjection/WebpackEncoreExtension.php b/src/DependencyInjection/WebpackEncoreExtension.php
index 521a2e97..53a68dce 100644
--- a/src/DependencyInjection/WebpackEncoreExtension.php
+++ b/src/DependencyInjection/WebpackEncoreExtension.php
@@ -68,7 +68,9 @@ public function load(array $configs, ContainerBuilder $container)
}
$container->getDefinition('webpack_encore.tag_renderer')
- ->replaceArgument(2, $defaultAttributes);
+ ->replaceArgument(2, $defaultAttributes)
+ ->replaceArgument(3, $config['script_attributes'])
+ ->replaceArgument(4, $config['link_attributes']);
if ($config['preload']) {
if (!class_exists(AddLinkHeaderListener::class)) {
diff --git a/src/Event/RenderAssetTagEvent.php b/src/Event/RenderAssetTagEvent.php
new file mode 100644
index 00000000..bcffa85c
--- /dev/null
+++ b/src/Event/RenderAssetTagEvent.php
@@ -0,0 +1,64 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\WebpackEncoreBundle\Event;
+
+/**
+ * Dispatched each time a script or link tag is rendered.
+ */
+final class RenderAssetTagEvent
+{
+ public const TYPE_SCRIPT = 'script';
+ public const TYPE_LINK = 'link';
+
+ private $type;
+ private $url;
+ private $attributes;
+
+ public function __construct(string $type, string $url, array $attributes)
+ {
+ $this->type = $type;
+ $this->url = $url;
+ $this->attributes = $attributes;
+ }
+
+ public function isScriptTag(): bool
+ {
+ return $this->type === self::TYPE_SCRIPT;
+ }
+
+ public function isLinkTag(): bool
+ {
+ return $this->type === self::TYPE_LINK;
+ }
+
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ public function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * @param string $name The attribute name
+ * @param string|bool $value Value can be "true" to have an attribute without a value (e.g. "defer")
+ */
+ public function setAttribute(string $name, $value): void
+ {
+ $this->attributes[$name] = $value;
+ }
+
+ public function removeAttribute(string $name): void
+ {
+ unset($this->attributes[$name]);
+ }
+}
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index 2464c368..cb03623a 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -17,7 +17,10 @@
-
+
+
+
+
diff --git a/src/Twig/EntryFilesTwigExtension.php b/src/Twig/EntryFilesTwigExtension.php
index 30254cb3..5b6bc1bf 100644
--- a/src/Twig/EntryFilesTwigExtension.php
+++ b/src/Twig/EntryFilesTwigExtension.php
@@ -46,16 +46,16 @@ public function getWebpackCssFiles(string $entryName, string $entrypointName = '
->getCssFiles($entryName);
}
- public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
+ public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default', array $attributes = []): string
{
return $this->getTagRenderer()
- ->renderWebpackScriptTags($entryName, $packageName, $entrypointName);
+ ->renderWebpackScriptTags($entryName, $packageName, $entrypointName, $attributes);
}
- public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
+ public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default', array $attributes = []): string
{
return $this->getTagRenderer()
- ->renderWebpackLinkTags($entryName, $packageName, $entrypointName);
+ ->renderWebpackLinkTags($entryName, $packageName, $entrypointName, $attributes);
}
private function getEntrypointLookup(string $entrypointName): EntrypointLookupInterface
diff --git a/tests/Asset/TagRendererTest.php b/tests/Asset/TagRendererTest.php
index 9279c4ee..e4fae3dd 100644
--- a/tests/Asset/TagRendererTest.php
+++ b/tests/Asset/TagRendererTest.php
@@ -11,9 +11,11 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Asset\Packages;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollection;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
+use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
use Symfony\WebpackEncoreBundle\Tests\TestEntrypointLookupIntegrityDataProviderInterface;
class TagRendererTest extends TestCase
@@ -40,15 +42,79 @@ public function testRenderScriptTagsWithDefaultAttributes()
->willReturnCallback(function ($path) {
return 'http://localhost:8080'.$path;
});
- $renderer = new TagRenderer($entrypointCollection, $packages, []);
+ $renderer = new TagRenderer($entrypointCollection, $packages, ['defer' => true]);
$output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
$this->assertStringContainsString(
- '',
+ '',
$output
);
$this->assertStringContainsString(
- '',
+ '',
+ $output
+ );
+ }
+
+ public function testRenderScriptTagsWithExtraAttributes()
+ {
+ $entrypointLookup = $this->createMock(EntrypointLookupInterface::class);
+ $entrypointLookup->expects($this->once())
+ ->method('getJavaScriptFiles')
+ ->willReturn(['/build/file1.js']);
+ $entrypointCollection = $this->createMock(EntrypointLookupCollection::class);
+ $entrypointCollection->expects($this->once())
+ ->method('getEntrypointLookup')
+ ->willReturn($entrypointLookup);
+
+ $packages = $this->createMock(Packages::class);
+ $packages->expects($this->exactly(1))
+ ->method('getUrl')
+ ->willReturn('http://localhost:8080/build/file1.js');
+ $renderer = new TagRenderer($entrypointCollection, $packages, [
+ 'defer' => true,
+ 'nonce' => 'abc123'
+ ], ['referrerpolicy' => 'origin']);
+
+ $output = $renderer->renderWebpackScriptTags('my_entry', null, null, [
+ // override the attribute
+ 'nonce' => '12345',
+ ]);
+ $this->assertStringContainsString(
+ '',
+ $output
+ );
+ }
+
+ public function testRenderScriptTagsDispatchesAnEvent()
+ {
+ $entrypointLookup = $this->createMock(EntrypointLookupInterface::class);
+ $entrypointLookup->expects($this->once())
+ ->method('getJavaScriptFiles')
+ ->willReturn(['/build/file1.js']);
+ $entrypointCollection = $this->createMock(EntrypointLookupCollection::class);
+ $entrypointCollection->expects($this->once())
+ ->method('getEntrypointLookup')
+ ->willReturn($entrypointLookup);
+
+ $packages = $this->createMock(Packages::class);
+ $packages->expects($this->exactly(1))
+ ->method('getUrl')
+ ->willReturn('http://localhost:8080/build/file1.js');
+
+ $event = new RenderAssetTagEvent(RenderAssetTagEvent::TYPE_SCRIPT, 'http://foo', [
+ 'src' => 'http://localhost:8080/build/file1.js',
+ 'nonce' => 'some_nonce_here',
+ ]);
+ $dispatcher = $this->createMock(EventDispatcherInterface::class);
+ $dispatcher->expects($this->once())
+ ->method('dispatch')
+ ->willReturn($event);
+
+ $renderer = new TagRenderer($entrypointCollection, $packages, [], [], [], $dispatcher);
+
+ $output = $renderer->renderWebpackScriptTags('my_entry');
+ $this->assertStringContainsString(
+ '',
$output
);
}
@@ -75,7 +141,7 @@ public function testRenderScriptTagsWithBadFilename()
$output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
$this->assertStringContainsString(
- '',
+ '',
$output
);
}
@@ -121,17 +187,17 @@ public function testRenderScriptTagsWithinAnEntryPointCollection()
$output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
$this->assertStringContainsString(
- '',
+ '',
$output
);
$output = $renderer->renderWebpackScriptTags('my_entry', null, 'second');
$this->assertStringContainsString(
- '',
+ '',
$output
);
$output = $renderer->renderWebpackScriptTags('my_entry', 'specific_package', 'third');
$this->assertStringContainsString(
- '',
+ '',
$output
);
}
@@ -168,11 +234,11 @@ public function testRenderScriptTagsWithHashes()
$output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
$this->assertStringContainsString(
- '',
+ '',
$output
);
$this->assertStringContainsString(
- '',
+ '',
$output
);
}
diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php
index 2cf8da5d..2ff1b78d 100644
--- a/tests/IntegrationTest.php
+++ b/tests/IntegrationTest.php
@@ -35,12 +35,13 @@ class IntegrationTest extends TestCase
public function testTwigIntegration()
{
$kernel = new WebpackEncoreIntegrationTestKernel(true);
+ $kernel->scriptAttributes = ['referrerpolicy' => 'origin'];
$kernel->boot();
$twig = $this->getTwigEnvironmentFromBootedKernel($kernel);
$html1 = $twig->render('@integration_test/template.twig');
$this->assertStringContainsString(
- '',
+ '',
$html1
);
$this->assertStringContainsString(
@@ -49,7 +50,7 @@ public function testTwigIntegration()
$html1
);
$this->assertStringContainsString(
- '',
+ '',
$html1
);
$this->assertStringContainsString(
@@ -118,7 +119,7 @@ public function testCacheWarmer()
public function testEnabledStrictMode_throwsException_ifBuildMissing()
{
$this->expectException(\Twig\Error\RuntimeError::class);
- $this->expectExceptionMessageRegExp('/Could not find the entrypoints file/');
+ $this->expectExceptionMessage('Could not find the entrypoints file from Webpack: the file "missing_build/entrypoints.json" does not exist.');
$kernel = new WebpackEncoreIntegrationTestKernel(true);
$kernel->outputPath = 'missing_build';
@@ -208,6 +209,7 @@ abstract class AbstractWebpackEncoreIntegrationTestKernel extends Kernel
public $builds = [
'different_build' => __DIR__.'/fixtures/different_build',
];
+ public $scriptAttributes = [];
public function __construct(bool $enableAssets)
{
@@ -255,6 +257,7 @@ protected function configureContainer(ContainerBuilder $container, LoaderInterfa
'preload' => true,
'builds' => $this->builds,
'strict_mode' => $this->strictMode,
+ 'script_attributes' => $this->scriptAttributes,
]);
$container->register(WebpackEncoreCacheWarmerTester::class)
diff --git a/tests/fixtures/template.twig b/tests/fixtures/template.twig
index 11b66b92..5130c1dc 100644
--- a/tests/fixtures/template.twig
+++ b/tests/fixtures/template.twig
@@ -1,4 +1,6 @@
-{{ encore_entry_script_tags('my_entry') }}
+{{ encore_entry_script_tags('my_entry', attributes={
+ defer: true
+}) }}
{{ encore_entry_link_tags('my_entry') }}
{{ encore_entry_script_tags('third_entry', null, 'different_build') }}
{{ encore_entry_link_tags('third_entry', null, 'different_build') }}