* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\AssetMapper\ImportMap; use Psr\Link\EvolvableLinkProviderInterface; use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; use Symfony\Component\WebLink\GenericLinkProvider; use Symfony\Component\WebLink\Link; /** * @author Kévin Dunglas * @author Ryan Weaver * * @final */ class ImportMapRenderer { // https://generator.jspm.io/#S2NnYGAIzSvJLMlJTWEAAMYOgCAOAA 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'; public function __construct( private readonly ImportMapGenerator $importMapGenerator, private readonly ?Packages $assetPackages = null, private readonly string $charset = 'UTF-8', private readonly string|false $polyfillImportName = false, private readonly array $scriptAttributes = [], private readonly ?RequestStack $requestStack = null, ) { } public function render(string|array $entryPoint, array $attributes = []): string { $entryPoint = (array) $entryPoint; $importMapData = $this->importMapGenerator->getImportMapData($entryPoint); $importMap = []; $modulePreloads = []; $cssLinks = []; $polyfillPath = null; foreach ($importMapData as $importName => $data) { $path = $data['path']; if ($this->assetPackages) { // ltrim so the subdirectory (if needed) can be prepended $path = $this->assetPackages->getUrl(ltrim($path, '/')); } // if this represents the polyfill, hide it from the import map if ($importName === $this->polyfillImportName) { $polyfillPath = $path; continue; } // for subdirectories or CDNs, the import name needs to be the full URL if (str_starts_with($importName, '/') && $this->assetPackages) { $importName = $this->assetPackages->getUrl(ltrim($importName, '/')); } $preload = $data['preload'] ?? false; if ('css' !== $data['type']) { $importMap[$importName] = $path; if ($preload) { $modulePreloads[] = $path; } } elseif ($preload) { $cssLinks[] = $path; // 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))); } } $output = ''; foreach ($cssLinks as $url) { $url = $this->escapeAttributeValue($url); $output .= "\n"; } if (class_exists(AddLinkHeaderListener::class) && $request = $this->requestStack?->getCurrentRequest()) { $this->addWebLinkPreloads($request, $cssLinks); } $scriptAttributes = $this->createAttributesString($attributes); $importMapJson = json_encode(['imports' => $importMap], \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG); $output .= << $importMapJson HTML; if (false !== $this->polyfillImportName && null === $polyfillPath) { if ('es-module-shims' !== $this->polyfillImportName) { throw new \InvalidArgumentException(sprintf('The JavaScript module polyfill was not found in your import map. Either disable the polyfill or run "php bin/console importmap:require "%s"" to install it.', $this->polyfillImportName)); } // a fallback for the default polyfill in case it's not in the importmap $polyfillPath = self::DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL; } if ($polyfillPath) { $url = $this->escapeAttributeValue($polyfillPath); $polyfillAttributes = $scriptAttributes; // Add security attributes for the default polyfill hosted on jspm.io if (self::DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL === $polyfillPath) { $polyfillAttributes = $this->createAttributesString([ 'crossorigin' => 'anonymous', 'integrity' => self::DEFAULT_ES_MODULE_SHIMS_POLYFILL_INTEGRITY, ] + $attributes); } $output .= << HTML; } foreach ($modulePreloads as $url) { $url = $this->escapeAttributeValue($url); $output .= "\n"; } if (\count($entryPoint) > 0) { $output .= "\n'; } return $output; } private function escapeAttributeValue(string $value): string { return htmlspecialchars($value, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); } private function createAttributesString(array $attributes): string { $attributeString = ''; $attributes += $this->scriptAttributes; if (isset($attributes['src']) || isset($attributes['type'])) { throw new \InvalidArgumentException(sprintf('The "src" and "type" attributes are not allowed on the