diff --git a/src/Twig/PwaRuntime.php b/src/Twig/PwaRuntime.php index 5752243..209b81b 100644 --- a/src/Twig/PwaRuntime.php +++ b/src/Twig/PwaRuntime.php @@ -4,12 +4,16 @@ namespace SpomkyLabs\PwaBundle\Twig; +use InvalidArgumentException; use SpomkyLabs\PwaBundle\Dto\Icon; use SpomkyLabs\PwaBundle\Dto\Manifest; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Mime\MimeTypes; +use const ENT_COMPAT; +use const ENT_SUBSTITUTE; use const PHP_EOL; final readonly class PwaRuntime @@ -17,6 +21,8 @@ private string $manifestPublicUrl; public function __construct( + #[Autowire('@asset_mapper.importmap.config_reader')] + private ImportMapConfigReader $importMapConfigReader, private AssetMapperInterface $assetMapper, private Manifest $manifest, #[Autowire('%spomky_labs_pwa.manifest_public_url%')] @@ -25,28 +31,86 @@ public function __construct( $this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/'); } - public function load(bool $themeColor = true, bool $icons = true): string - { + /** + * @param array $swAttributes + */ + public function load( + bool $injectThemeColor = true, + bool $injectIcons = true, + bool $injectSW = true, + array $swAttributes = [] + ): string { $url = $this->assetMapper->getPublicPath($this->manifestPublicUrl) ?? $this->manifestPublicUrl; - $output = sprintf('', $url); - if ($this->manifest->icons !== [] && $icons === true) { - foreach ($this->manifest->icons as $icon) { - ['url' => $url, 'format' => $format] = $this->getIconInfo($icon); - $attributes = sprintf( - 'rel="%s" sizes="%s" href="%s"', - str_contains($icon->purpose ?? '', 'maskable') ? 'mask-icon' : 'icon', - $icon->getSizeList(), - $url - ); - if ($format !== null) { - $attributes .= sprintf(' type="%s"', $format); - } - - $output .= sprintf('%s', PHP_EOL, $attributes); - } + $output = sprintf('%s', PHP_EOL, $url); + $output = $this->injectIcons($output, $injectIcons); + $output = $this->injectThemeColor($output, $injectThemeColor); + + return $this->injectServiceWorker($output, $injectSW, $swAttributes); + } + + private function injectThemeColor(string $output, bool $themeColor): string + { + if ($this->manifest->themeColor === null || $themeColor === false) { + return $output; + } + + return $output . sprintf('%s', PHP_EOL, $this->manifest->themeColor); + } + + /** + * @param array $swAttributes + */ + private function injectServiceWorker(string $output, bool $injectSW, array $swAttributes): string + { + $serviceWorker = $this->manifest->serviceWorker; + if ($serviceWorker === null || $injectSW === false) { + return $output; + } + $scriptAttributes = $this->createAttributesString($swAttributes); + $url = $serviceWorker->dest; + $registerOptions = ''; + if ($serviceWorker->scope !== null) { + $registerOptions .= sprintf(", scope: '%s'", $serviceWorker->scope); + } + if ($serviceWorker->useCache !== null) { + $registerOptions .= sprintf(', useCache: %s', $serviceWorker->useCache ? 'true' : 'false'); } - if ($this->manifest->themeColor !== null && $themeColor === true) { - $output .= sprintf('%s', PHP_EOL, $this->manifest->themeColor); + if ($registerOptions !== '') { + $registerOptions = sprintf(', {%s}', mb_substr($registerOptions, 2)); + } + $hasWorkboxWindow = $this->importMapConfigReader->findRootImportMapEntry('workbox-window') !== null; + $workboxUrl = $hasWorkboxWindow ? 'workbox-window' : 'https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-window.prod.mjs'; + $declaration = << + import {Workbox} from '{$workboxUrl}'; + if ('serviceWorker' in navigator) { + const wb = new Workbox('{$url}'{$registerOptions}); + wb.register(); + } + +SERVICE_WORKER; + + return $output . sprintf('%s%s', PHP_EOL, $declaration); + } + + private function injectIcons(string $output, bool $injectIcons): string + { + if ($this->manifest->icons === [] || $injectIcons === false) { + return $output; + } + foreach ($this->manifest->icons as $icon) { + ['url' => $url, 'format' => $format] = $this->getIconInfo($icon); + $attributes = sprintf( + 'rel="%s" sizes="%s" href="%s"', + str_contains($icon->purpose ?? '', 'maskable') ? 'mask-icon' : 'icon', + $icon->getSizeList(), + $url + ); + if ($format !== null) { + $attributes .= sprintf(' type="%s"', $format); + } + + $output .= sprintf('%s', PHP_EOL, $attributes); } return $output; @@ -87,4 +151,30 @@ private function getFormat(Icon $object, ?MappedAsset $asset): ?string $mime = MimeTypes::getDefault(); return $mime->guessMimeType($asset->sourcePath); } + + private function createAttributesString(array $attributes): string + { + $attributeString = ''; + if (isset($attributes['src']) || isset($attributes['type'])) { + throw new InvalidArgumentException(sprintf( + 'The "src" and "type" attributes are not allowed on the