From 990a44cdf44e8f222ee4c01d5930ad0a036f2f4e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 16 Jul 2025 09:54:16 +0200 Subject: [PATCH] [AssetMapper] Add support for loading JSON using import statements --- .../Component/AssetMapper/CHANGELOG.md | 5 +++ .../Command/DebugAssetMapperCommand.php | 2 +- .../ImportMap/ImportMapConfigReader.php | 2 +- .../ImportMap/ImportMapManager.php | 7 +--- .../ImportMap/ImportMapRenderer.php | 39 ++++++++++++------- .../AssetMapper/ImportMap/ImportMapType.php | 1 + .../Tests/ImportMap/ImportMapRendererTest.php | 2 +- 7 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/AssetMapper/CHANGELOG.md b/src/Symfony/Component/AssetMapper/CHANGELOG.md index 93d622101c0c8..155472fb3aa5a 100644 --- a/src/Symfony/Component/AssetMapper/CHANGELOG.md +++ b/src/Symfony/Component/AssetMapper/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * Add support for loading JSON using import statements + 7.3 --- diff --git a/src/Symfony/Component/AssetMapper/Command/DebugAssetMapperCommand.php b/src/Symfony/Component/AssetMapper/Command/DebugAssetMapperCommand.php index a81857b5c14b2..4cbb9cb53b7ae 100644 --- a/src/Symfony/Component/AssetMapper/Command/DebugAssetMapperCommand.php +++ b/src/Symfony/Component/AssetMapper/Command/DebugAssetMapperCommand.php @@ -43,7 +43,7 @@ protected function configure(): void { $this ->addArgument('name', InputArgument::OPTIONAL, 'An asset name (or a path) to search for (e.g. "app")') - ->addOption('ext', null, InputOption::VALUE_REQUIRED, 'Filter assets by extension (e.g. "css")', null, ['js', 'css', 'png']) + ->addOption('ext', null, InputOption::VALUE_REQUIRED, 'Filter assets by extension (e.g. "css")', null, ['js', 'css', 'json']) ->addOption('full', null, null, 'Whether to show the full paths') ->addOption('vendor', null, InputOption::VALUE_NEGATABLE, 'Only show assets from vendor packages') ->setHelp(<<<'EOT' diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php index 4dc98fe394245..ca330b42d31b7 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php @@ -49,7 +49,7 @@ public function getEntries(): ImportMapEntries throw new \InvalidArgumentException(\sprintf('The following keys are not valid for the importmap entry "%s": "%s". Valid keys are: "%s".', $importName, implode('", "', $invalidKeys), implode('", "', $validKeys))); } - $type = isset($data['type']) ? ImportMapType::tryFrom($data['type']) : ImportMapType::JS; + $type = ImportMapType::tryFrom($data['type'] ?? 'js') ?? ImportMapType::JS; $isEntrypoint = $data['entrypoint'] ?? false; if (isset($data['path'])) { diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php index 00c265bc4635d..c63ae38a2e204 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php @@ -162,7 +162,7 @@ public function requirePackages(array $packagesToRequire, ImportMapEntries $impo $newEntry = ImportMapEntry::createLocal( $requireOptions->importName, - self::getImportMapTypeFromFilename($requireOptions->path), + ImportMapType::tryFrom(pathinfo($path, \PATHINFO_EXTENSION)) ?? ImportMapType::JS, $path, $requireOptions->entrypoint, ); @@ -200,11 +200,6 @@ private function cleanupPackageFiles(ImportMapEntry $entry): void } } - private static function getImportMapTypeFromFilename(string $path): ImportMapType - { - return str_ends_with($path, '.css') ? ImportMapType::CSS : ImportMapType::JS; - } - /** * Finds the MappedAsset allowing for a "logical path", relative or absolute filesystem path. */ diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php index 87d557f6d422f..603e3589ab569 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php @@ -31,6 +31,9 @@ class ImportMapRenderer private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js'; private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_INTEGRITY = 'sha384-ie1x72Xck445i0j4SlNJ5W5iGeL3Dpa0zD48MZopgWsjNB/lt60SuG1iduZGNnJn'; + private const LOADER_JSON = "export default (async()=>await(await fetch('%s')).json())()"; + private const LOADER_CSS = "document.head.appendChild(Object.assign(document.createElement('link'),{rel:'stylesheet',href:'%s'}))"; + public function __construct( private readonly ImportMapGenerator $importMapGenerator, private readonly ?Packages $assetPackages = null, @@ -48,7 +51,7 @@ public function render(string|array $entryPoint, array $attributes = []): string $importMapData = $this->importMapGenerator->getImportMapData($entryPoint); $importMap = []; $modulePreloads = []; - $cssLinks = []; + $webLinks = []; $polyfillPath = null; foreach ($importMapData as $importName => $data) { $path = $data['path']; @@ -70,29 +73,34 @@ public function render(string|array $entryPoint, array $attributes = []): string } $preload = $data['preload'] ?? false; - if ('css' !== $data['type']) { + if ('json' === $data['type']) { + $importMap[$importName] = 'data:application/javascript,'.str_replace('%', '%25', \sprintf(self::LOADER_JSON, addslashes($path))); + if ($preload) { + $webLinks[$path] = 'fetch'; + } + } elseif ('css' !== $data['type']) { $importMap[$importName] = $path; if ($preload) { $modulePreloads[] = $path; } } elseif ($preload) { - $cssLinks[] = $path; + $webLinks[$path] = 'style'; // importmap entry is a noop $importMap[$importName] = 'data:application/javascript,'; } else { - $importMap[$importName] = 'data:application/javascript,'.rawurlencode(\sprintf('document.head.appendChild(Object.assign(document.createElement("link"),{rel:"stylesheet",href:"%s"}))', addslashes($path))); + $importMap[$importName] = 'data:application/javascript,'.str_replace('%', '%25', \sprintf(self::LOADER_CSS, addslashes($path))); } } $output = ''; - foreach ($cssLinks as $url) { - $url = $this->escapeAttributeValue($url); - - $output .= "\n"; + foreach ($webLinks as $url => $as) { + if ('style' === $as) { + $output .= "\nescapeAttributeValue($url)}\">"; + } } if (class_exists(AddLinkHeaderListener::class) && $request = $this->requestStack?->getCurrentRequest()) { - $this->addWebLinkPreloads($request, $cssLinks); + $this->addWebLinkPreloads($request, $webLinks); } $scriptAttributes = $attributes || $this->scriptAttributes ? ' '.$this->createAttributesString($attributes) : ''; @@ -186,12 +194,17 @@ private function createAttributesString(array $attributes, string $pattern = '%s return $attributeString; } - private function addWebLinkPreloads(Request $request, array $cssLinks): void + private function addWebLinkPreloads(Request $request, array $links): void { - $cssPreloadLinks = array_map(fn ($url) => (new Link('preload', $url))->withAttribute('as', 'style'), $cssLinks); + foreach ($links as $url => $as) { + $links[$url] = (new Link('preload', $url))->withAttribute('as', $as); + if ('fetch' === $as) { + $links[$url] = $links[$url]->withAttribute('crossorigin', 'anonymous'); + } + } if (null === $linkProvider = $request->attributes->get('_links')) { - $request->attributes->set('_links', new GenericLinkProvider($cssPreloadLinks)); + $request->attributes->set('_links', new GenericLinkProvider($links)); return; } @@ -200,7 +213,7 @@ private function addWebLinkPreloads(Request $request, array $cssLinks): void return; } - foreach ($cssPreloadLinks as $link) { + foreach ($links as $link) { $linkProvider = $linkProvider->withLink($link); } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapType.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapType.php index 99c8ff270c739..e2453efd45af0 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapType.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapType.php @@ -15,4 +15,5 @@ enum ImportMapType: string { case JS = 'js'; case CSS = 'css'; + case JSON = 'json'; } diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php index ef519ff719b4b..7b87527641290 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php @@ -92,7 +92,7 @@ public function testBasicRender() $this->assertStringContainsString('"app_css_preload": "data:application/javascript,', $html); $this->assertStringContainsString('', $html); // non-preloaded CSS file - $this->assertStringContainsString('"app_css_no_preload": "data:application/javascript,document.head.appendChild%28Object.assign%28document.createElement%28%22link%22%29%2C%7Brel%3A%22stylesheet%22%2Chref%3A%22%2Fsubdirectory%2Fassets%2Fstyles%2Fapp-nopreload-d1g35t.css%22%7D', $html); + $this->assertStringContainsString('"app_css_no_preload": "data:application/javascript,document.head.appendChild(Object.assign(document.createElement(\'link\'),{rel:\'stylesheet\',href:\'/subdirectory/assets/styles/app-nopreload-d1g35t.css\'}))', $html); $this->assertStringNotContainsString('', $html); // remote js $this->assertStringContainsString('"remote_js": "https://cdn.example.com/assets/remote-d1g35t.js"', $html); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy