diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index b9986a8..e446f6e 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -15,6 +15,8 @@ jobs: steps: - name: "Checkout" uses: "actions/checkout@v4" + with: + fetch-depth: 0 - name: "Release" uses: "laminas/automatic-releases@1.24.0" @@ -36,6 +38,8 @@ jobs: steps: - name: "Checkout" uses: "actions/checkout@v4" + with: + fetch-depth: 0 - name: "Create Merge-Up Pull Request" uses: "laminas/automatic-releases@1.24.0" @@ -57,6 +61,8 @@ jobs: steps: - name: "Checkout" uses: "actions/checkout@v4" + with: + fetch-depth: 0 - name: "Create and/or Switch to new Release Branch" uses: "laminas/automatic-releases@1.24.0" @@ -101,6 +107,8 @@ jobs: steps: - name: "Checkout" uses: "actions/checkout@v4" + with: + fetch-depth: 0 - name: "Create new milestones" uses: "laminas/automatic-releases@1.24.0" diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index b036f21..be47bf5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -175,10 +175,15 @@ private function setupServiceWorker(ArrayNodeDefinition $node): void ->example('/\.(ico|png|jpe?g|gif|svg|webp|bmp)$/') ->end() ->scalarNode('static_regex') - ->defaultValue('/\.(css|js|json|xml|txt|woff2|ttf|eot|otf|map|webmanifest)$/') + ->defaultValue('/\.(css|js|json|xml|txt|map|webmanifest)$/') ->info('The regex to match the static files.') ->example('/\.(css|js|json|xml|txt|woff2|ttf|eot|otf|map|webmanifest)$/') ->end() + ->scalarNode('font_regex') + ->defaultValue('/\.(ttf|eot|otf|woff2)$/') + ->info('The regex to match the static files.') + ->example('/\.(ttf|eot|otf|woff2)$/') + ->end() ->integerNode('max_image_cache_entries') ->defaultValue(60) ->info('The maximum number of entries in the image cache.') diff --git a/src/Dto/Workbox.php b/src/Dto/Workbox.php index 945f8b8..62313a7 100644 --- a/src/Dto/Workbox.php +++ b/src/Dto/Workbox.php @@ -64,7 +64,10 @@ final class Workbox public string $imageRegex = '/\.(ico|png|jpe?g|gif|svg|webp|bmp)$/'; #[SerializedName('static_regex')] - public string $staticRegex = '/\.(css|m?jsx?|json|xml|txt|woff2|ttf|eot|otf|map|webmanifest)$/'; + public string $staticRegex = '/\.(css|m?jsx?|json|xml|txt|map|webmanifest)$/'; + + #[SerializedName('font_regex')] + public string $fontRegex = '/\.(ttf|eot|otf|woff2)$/'; #[SerializedName('clear_cache')] public bool $clearCache = true; diff --git a/src/Service/ServiceWorkerCompiler.php b/src/Service/ServiceWorkerCompiler.php index e482c08..2ed843a 100644 --- a/src/Service/ServiceWorkerCompiler.php +++ b/src/Service/ServiceWorkerCompiler.php @@ -130,53 +130,108 @@ private function processStandardRules(Workbox $workbox, string $body): string return $body; } - $images = []; - $statics = []; + $assets = []; + $fonts = []; foreach ($this->assetMapper->allAssets() as $asset) { - if (preg_match($workbox->imageRegex, $asset->sourcePath) === 1) { - $images[] = $asset->publicPath; - } elseif (preg_match($workbox->staticRegex, $asset->sourcePath) === 1) { - $statics[] = $asset->publicPath; + if (preg_match($workbox->imageRegex, $asset->sourcePath) === 1 || preg_match( + $workbox->staticRegex, + $asset->sourcePath + ) === 1) { + $assets[] = $asset->publicPath; + } elseif (preg_match($workbox->fontRegex, $asset->sourcePath) === 1) { + $fonts[] = $asset->publicPath; } } $jsonOptions = [ JsonEncode::OPTIONS => JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR, ]; - $imageUrls = $this->serializer->serialize($images, 'json', $jsonOptions); - $staticUrls = $this->serializer->serialize($statics, 'json', $jsonOptions); + $assetUrls = $this->serializer->serialize($assets, 'json', $jsonOptions); + $fontUrls = $this->serializer->serialize($fonts, 'json', $jsonOptions); + $assetUrlsLength = count($assets) * 2; $routes = $this->serializer->serialize($workbox->warmCacheUrls, 'json', $jsonOptions); $declaration = <<pageCacheName}', networkTimeoutSeconds: {$workbox->networkTimeoutSeconds}, warmCache: {$routes} }); -workbox.recipes.imageCache({ - cacheName: '{$workbox->imageCacheName}', - maxEntries: {$workbox->maxImageCacheEntries}, - maxImageAge: {$workbox->maxImageAge}, - warmCache: {$imageUrls} -}); -workbox.recipes.staticResourceCache({ - cacheName: '{$workbox->assetCacheName}', - warmCache: {$staticUrls} -}); + +//Images cache workbox.routing.registerRoute( - ({request}) => request.destination === 'font', + ({request, url}) => (request.destination === 'image' && !url.pathname.startsWith('/assets')), new workbox.strategies.CacheFirst({ - cacheName: '{$workbox->fontCacheName}', + cacheName: '{$workbox->imageCacheName}', plugins: [ - new workbox.cacheableResponse.CacheableResponsePlugin({ - statuses: [0, 200], - }), + new workbox.cacheableResponse.CacheableResponsePlugin({statuses: [0, 200]}), new workbox.expiration.ExpirationPlugin({ - maxAgeSeconds: {$workbox->maxFontAge}, - maxEntries: {$workbox->maxFontCacheEntries}, + maxEntries: {$workbox->maxImageCacheEntries}, + maxAgeSeconds: {$workbox->maxImageAge}, }), ], }) ); + +// Assets served by Asset Mapper +// - Strategy: CacheFirst +const assetCacheStrategy = new workbox.strategies.CacheFirst({ + cacheName: '{$workbox->assetCacheName}', + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({statuses: [0, 200]}), + new workbox.expiration.ExpirationPlugin({ + maxEntries: {$assetUrlsLength}, + maxAgeSeconds: 365 * 24 * 60 * 60, + }), + ], +}); +// - Strategy: only the Asset Mapper public route +workbox.routing.registerRoute( + ({url}) => url.pathname.startsWith('/assets'), + assetCacheStrategy +); +self.addEventListener('install', event => { + const done = {$assetUrls}.map( + path => + assetCacheStrategy.handleAll({ + event, + request: new Request(path), + })[1] + ); + + event.waitUntil(Promise.all(done)); +}); + + +const fontCacheStrategy = new workbox.strategies.CacheFirst({ + cacheName: '{$workbox->fontCacheName}', + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({ + statuses: [0, 200], + }), + new workbox.expiration.ExpirationPlugin({ + maxAgeSeconds: {$workbox->maxFontAge}, + maxEntries: {$workbox->maxFontCacheEntries}, + }), + ], +}); +workbox.routing.registerRoute( + ({request}) => request.destination === 'font', + fontCacheStrategy +); +self.addEventListener('install', event => { + const done = {$fontUrls}.map( + path => + fontCacheStrategy.handleAll({ + event, + request: new Request(path), + })[1] + ); + + event.waitUntil(Promise.all(done)); +}); + + STANDARD_RULE_STRATEGY; return str_replace($workbox->standardRulesPlaceholder, trim($declaration), $body); diff --git a/src/Twig/PwaRuntime.php b/src/Twig/PwaRuntime.php index 2b659a2..0792016 100644 --- a/src/Twig/PwaRuntime.php +++ b/src/Twig/PwaRuntime.php @@ -21,6 +21,10 @@ private string $manifestPublicUrl; public function __construct( + #[Autowire('%spomky_labs_pwa.manifest.enabled%')] + private bool $manifestEnabled, + #[Autowire('%spomky_labs_pwa.sw.enabled%')] + private bool $serviceWorkerEnabled, #[Autowire('@asset_mapper.importmap.config_reader')] private ImportMapConfigReader $importMapConfigReader, private AssetMapperInterface $assetMapper, @@ -40,12 +44,23 @@ public function load( bool $injectSW = true, array $swAttributes = [] ): string { - $url = $this->assetMapper->getPublicPath($this->manifestPublicUrl) ?? $this->manifestPublicUrl; - $output = sprintf('%s', PHP_EOL, $url); + $output = ''; + if ($this->manifestEnabled === true) { + $output = $this->injectManifestFile($output); + } + if ($this->serviceWorkerEnabled === true) { + $output = $this->injectServiceWorker($output, $injectSW, $swAttributes); + } $output = $this->injectIcons($output, $injectIcons); - $output = $this->injectThemeColor($output, $injectThemeColor); - return $this->injectServiceWorker($output, $injectSW, $swAttributes); + return $this->injectThemeColor($output, $injectThemeColor); + } + + private function injectManifestFile(string $output): string + { + $url = $this->assetMapper->getPublicPath($this->manifestPublicUrl) ?? $this->manifestPublicUrl; + + return $output . sprintf('%s', PHP_EOL, $url); } private function injectThemeColor(string $output, bool $themeColor): string