Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e71a3a1

Browse filesBrowse files
weaverryanfabpot
authored andcommitted
[Asset] [AssetMapper] New AssetMapper component: Map assets to publicly available, versioned paths
1 parent ff98eff commit e71a3a1
Copy full SHA for e71a3a1

File tree

Expand file treeCollapse file tree

82 files changed

+4711
-2
lines changed
Filter options

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner
Expand file treeCollapse file tree

82 files changed

+4711
-2
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
},
5959
"replace": {
6060
"symfony/asset": "self.version",
61+
"symfony/asset-mapper": "self.version",
6162
"symfony/browser-kit": "self.version",
6263
"symfony/cache": "self.version",
6364
"symfony/clock": "self.version",
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Bridge\Twig\Extension;
13+
14+
use Twig\Extension\AbstractExtension;
15+
use Twig\TwigFunction;
16+
17+
/**
18+
* @author Kévin Dunglas <kevin@dunglas.dev>
19+
*/
20+
final class ImportMapExtension extends AbstractExtension
21+
{
22+
public function getFunctions(): array
23+
{
24+
return [
25+
new TwigFunction('importmap', [ImportMapRuntime::class, 'importmap'], ['is_safe' => ['html']]),
26+
];
27+
}
28+
}
+29Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Bridge\Twig\Extension;
13+
14+
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
15+
16+
/**
17+
* @author Kévin Dunglas <kevin@dunglas.dev>
18+
*/
19+
class ImportMapRuntime
20+
{
21+
public function __construct(private readonly ImportMapRenderer $importMapRenderer)
22+
{
23+
}
24+
25+
public function importmap(?string $entryPoint = 'app'): string
26+
{
27+
return $this->importMapRenderer->render($entryPoint);
28+
}
29+
}
+49Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 Extension;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\Twig\Extension\ImportMapExtension;
16+
use Symfony\Bridge\Twig\Extension\ImportMapRuntime;
17+
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
18+
use Twig\Environment;
19+
use Twig\Loader\ArrayLoader;
20+
use Twig\RuntimeLoader\RuntimeLoaderInterface;
21+
22+
class ImportMapExtensionTest extends TestCase
23+
{
24+
public function testItRendersTheImportmap()
25+
{
26+
$twig = new Environment(new ArrayLoader([
27+
'template' => '{{ importmap("application") }}',
28+
]), ['debug' => true, 'cache' => false, 'autoescape' => 'html', 'optimizations' => 0]);
29+
$twig->addExtension(new ImportMapExtension());
30+
$importMapRenderer = $this->createMock(ImportMapRenderer::class);
31+
$expected = '<script type="importmap">{ "imports": {}}</script>';
32+
$importMapRenderer->expects($this->once())
33+
->method('render')
34+
->with('application')
35+
->willReturn($expected);
36+
$runtime = new ImportMapRuntime($importMapRenderer);
37+
38+
$mockRuntimeLoader = $this->createMock(RuntimeLoaderInterface::class);
39+
$mockRuntimeLoader
40+
->method('load')
41+
->willReturnMap([
42+
[ImportMapRuntime::class, $runtime],
43+
])
44+
;
45+
$twig->addRuntimeLoader($mockRuntimeLoader);
46+
47+
$this->assertSame($expected, $twig->render('template'));
48+
}
49+
}

‎src/Symfony/Bridge/Twig/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Twig/composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"league/html-to-markdown": "^5.0",
2727
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
2828
"symfony/asset": "^5.4|^6.0",
29+
"symfony/asset-mapper": "^6.3",
2930
"symfony/dependency-injection": "^5.4|^6.0",
3031
"symfony/finder": "^5.4|^6.0",
3132
"symfony/form": "^6.3",

‎src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
+15-1Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Psr\Container\ContainerInterface;
1515
use Psr\Link\EvolvableLinkInterface;
1616
use Psr\Link\LinkInterface;
17+
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
1718
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
1819
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
1920
use Symfony\Component\Form\Extension\Core\Type\FormType;
@@ -44,6 +45,7 @@
4445
use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener;
4546
use Symfony\Component\WebLink\GenericLinkProvider;
4647
use Symfony\Component\WebLink\HttpHeaderSerializer;
48+
use Symfony\Component\WebLink\Link;
4749
use Symfony\Contracts\Service\Attribute\Required;
4850
use Symfony\Contracts\Service\ServiceSubscriberInterface;
4951
use Twig\Environment;
@@ -95,6 +97,7 @@ public static function getSubscribedServices(): array
9597
'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class,
9698
'parameter_bag' => '?'.ContainerBagInterface::class,
9799
'web_link.http_header_serializer' => '?'.HttpHeaderSerializer::class,
100+
'asset_mapper.importmap.manager' => '?'.ImportMapManager::class,
98101
];
99102
}
100103

@@ -409,7 +412,7 @@ protected function addLink(Request $request, LinkInterface $link): void
409412
/**
410413
* @param LinkInterface[] $links
411414
*/
412-
protected function sendEarlyHints(iterable $links, Response $response = null): Response
415+
protected function sendEarlyHints(iterable $links = [], Response $response = null, bool $preloadJavaScriptModules = false): Response
413416
{
414417
if (!$this->container->has('web_link.http_header_serializer')) {
415418
throw new \LogicException('You cannot use the "sendEarlyHints" method if the WebLink component is not available. Try running "composer require symfony/web-link".');
@@ -418,6 +421,17 @@ protected function sendEarlyHints(iterable $links, Response $response = null): R
418421
$response ??= new Response();
419422

420423
$populatedLinks = [];
424+
425+
if ($preloadJavaScriptModules) {
426+
if (!$this->container->has('asset_mapper.importmap.manager')) {
427+
throw new \LogicException('You cannot use the JavaScript modules method if the AssetMapper component is not available. Try running "composer require symfony/asset-mapper".');
428+
}
429+
430+
foreach ($this->container->get('asset_mapper.importmap.manager')->getModulesToPreload() as $url) {
431+
$populatedLinks[] = new Link('modulepreload', $url);
432+
}
433+
}
434+
421435
foreach ($links as $link) {
422436
if ($link instanceof EvolvableLinkInterface && !$link->getRels()) {
423437
$link = $link->withRel('preload');

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class UnusedTagsPass implements CompilerPassInterface
2424
private const KNOWN_TAGS = [
2525
'annotations.cached_reader',
2626
'assets.package',
27+
'asset_mapper.compiler',
2728
'auto_alias',
2829
'cache.pool',
2930
'cache.pool.clearer',

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+94Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Psr\Log\LogLevel;
1717
use Symfony\Bundle\FullStack;
1818
use Symfony\Component\Asset\Package;
19+
use Symfony\Component\AssetMapper\AssetMapper;
20+
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
1921
use Symfony\Component\Cache\Adapter\DoctrineAdapter;
2022
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
2123
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
@@ -161,6 +163,7 @@ public function getConfigTreeBuilder(): TreeBuilder
161163
$this->addSessionSection($rootNode);
162164
$this->addRequestSection($rootNode);
163165
$this->addAssetsSection($rootNode, $enableIfStandalone);
166+
$this->addAssetMapperSection($rootNode, $enableIfStandalone);
164167
$this->addTranslatorSection($rootNode, $enableIfStandalone);
165168
$this->addValidationSection($rootNode, $enableIfStandalone);
166169
$this->addAnnotationsSection($rootNode, $willBeAvailable);
@@ -810,6 +813,97 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl
810813
;
811814
}
812815

816+
private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void
817+
{
818+
$rootNode
819+
->children()
820+
->arrayNode('asset_mapper')
821+
->info('Asset Mapper configuration')
822+
->{$enableIfStandalone('symfony/asset-mapper', AssetMapper::class)}()
823+
->fixXmlConfig('path')
824+
->fixXmlConfig('extension')
825+
->fixXmlConfig('importmap_script_attribute')
826+
->children()
827+
// add array node called "paths" that will be an array of strings
828+
->arrayNode('paths')
829+
->info('Directories that hold assets that should be in the mapper. Can be a simple array of an array of ["path/to/assets": "namespace"]')
830+
->example(['assets/'])
831+
->normalizeKeys(false)
832+
->useAttributeAsKey('namespace')
833+
->beforeNormalization()
834+
->always()
835+
->then(function ($v) {
836+
$result = [];
837+
foreach ($v as $key => $item) {
838+
// "dir" => "namespace"
839+
if (\is_string($key)) {
840+
$result[$key] = $item;
841+
842+
continue;
843+
}
844+
845+
if (\is_array($item)) {
846+
// $item = ["namespace" => "the/namespace", "value" => "the/dir"]
847+
$result[$item['value']] = $item['namespace'] ?? '';
848+
} else {
849+
// $item = "the/dir"
850+
$result[$item] = '';
851+
}
852+
}
853+
854+
return $result;
855+
})
856+
->end()
857+
->prototype('scalar')->end()
858+
->end()
859+
->booleanNode('server')
860+
->info('If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default)')
861+
->defaultValue($this->debug)
862+
->end()
863+
->scalarNode('public_prefix')
864+
->info('The public path where the assets will be written to (and served from when "server" is true)')
865+
->defaultValue('/assets/')
866+
->end()
867+
->booleanNode('strict_mode')
868+
->info('If true, an exception will be thrown if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import \'./non-existent.js\'"')
869+
->defaultValue(true)
870+
->end()
871+
->arrayNode('extensions')
872+
->info('Key-value pair of file extensions set to their mime type.')
873+
->normalizeKeys(false)
874+
->useAttributeAsKey('extension')
875+
->example(['.zip' => 'application/zip'])
876+
->prototype('scalar')->end()
877+
->end()
878+
->scalarNode('importmap_path')
879+
->info('The path of the importmap.php file.')
880+
->defaultValue('%kernel.project_dir%/importmap.php')
881+
->end()
882+
->scalarNode('importmap_polyfill')
883+
->info('URL of the ES Module Polyfill to use, false to disable. Defaults to using a CDN URL.')
884+
->defaultValue(null)
885+
->end()
886+
->arrayNode('importmap_script_attributes')
887+
->info('Key-value pair of attributes to add to script tags output for the importmap.')
888+
->normalizeKeys(false)
889+
->useAttributeAsKey('key')
890+
->example(['data-turbo-track' => 'reload'])
891+
->prototype('scalar')->end()
892+
->end()
893+
->scalarNode('vendor_dir')
894+
->info('The directory to store JavaScript vendors.')
895+
->defaultValue('%kernel.project_dir%/assets/vendor')
896+
->end()
897+
->scalarNode('provider')
898+
->info('The provider (CDN) to use', class_exists(ImportMapManager::class) ? sprintf(' (e.g.: "%s").', implode('", "', ImportMapManager::PROVIDERS)) : '.')
899+
->defaultValue('jspm')
900+
->end()
901+
->end()
902+
->end()
903+
->end()
904+
;
905+
}
906+
813907
private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void
814908
{
815909
$rootNode

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.