Skip to content

Commit

Permalink
Better SW
Browse files Browse the repository at this point in the history
  • Loading branch information
Spomky committed Jan 18, 2024
1 parent 382172e commit 55c981c
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 24 deletions.
19 changes: 18 additions & 1 deletion src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -476,12 +476,29 @@ private function setupServiceWorker(ArrayNodeDefinition $node): void
->treatFalseLike([])
->treatTrueLike([])
->treatNullLike([])
->beforeNormalization()
->ifString()
->then(static fn (string $v): array => [
'src' => $v,
])
->end()
->children()
->scalarNode('src')
->isRequired()
->info('The path to the service worker. Can be served by Asset Mapper.')
->info('The path to the service worker source file. Can be served by Asset Mapper.')
->example('script/sw.js')
->end()
->scalarNode('dest')
->cannotBeEmpty()
->defaultValue('/sw.js')
->info('The public URL to the service worker.')
->example('/sw.js')
->end()
->scalarNode('precaching_placeholder')
->defaultValue('self.__WB_MANIFEST')
->info('The placeholder for the precaching. Will be replaced by the assets and versions.')
->example('self.__WB_MANIFEST')
->end()
->scalarNode('scope')
->cannotBeEmpty()
->defaultValue('/')
Expand Down
6 changes: 6 additions & 0 deletions src/DependencyInjection/SpomkyLabsPwaExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ public function load(array $configs, ContainerBuilder $container): void
}
$container->setParameter('spomky_labs_pwa.routes.reference_type', $config['path_type_reference']);
$container->setParameter('spomky_labs_pwa.manifest_public_url', $config['manifest_public_url']);
$container->setParameter('spomky_labs_pwa.sw_public_url', $config['serviceworker']['dest'] ?? null);
$container->setParameter(
'spomky_labs_pwa.serviceworker.precaching_placeholder',
$config['serviceworker']['precaching_placeholder'] ?? 'self.__WB_MANIFEST'
);

unset(
$config['image_processor'],
$config['web_client'],
$config['path_type_reference'],
$config['manifest_public_url'],
$config['serviceworker']['precaching_placeholder'],
);
$container->setParameter('spomky_labs_pwa.config', $config);
if (! in_array($container->getParameter('kernel.environment'), ['dev', 'test'], true)) {
Expand Down
4 changes: 3 additions & 1 deletion src/Dto/ServiceWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

final class ServiceWorker
{
public null|string $src = null;
public string $src;

public string $dest;

public null|string $scope = null;

Expand Down
6 changes: 3 additions & 3 deletions src/Normalizer/ServiceWorkerNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public function normalize(mixed $object, string $format = null, array $context =
{
assert($object instanceof ServiceWorker);
$url = null;
if (! str_starts_with($object->src, '/')) {
$url = $this->assetMapper->getAsset($object->src)?->publicPath;
if (! str_starts_with($object->dest, '/')) {
$url = $this->assetMapper->getAsset($object->dest)?->publicPath;
}
if ($url === null) {
$url = $object->src;
$url = $object->dest;
}

$result = [
Expand Down
4 changes: 4 additions & 0 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor;
use SpomkyLabs\PwaBundle\ImageProcessor\ImagickImageProcessor;
use SpomkyLabs\PwaBundle\Service\Builder;
use SpomkyLabs\PwaBundle\Service\ServiceWorkerBuilder;
use SpomkyLabs\PwaBundle\Subscriber\AssetsCompileEventListener;
use SpomkyLabs\PwaBundle\Subscriber\PwaDevServerSubscriber;
use SpomkyLabs\PwaBundle\Subscriber\ServiceWorkerCompileEventListener;
use SpomkyLabs\PwaBundle\Twig\PwaExtension;
use SpomkyLabs\PwaBundle\Twig\PwaRuntime;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
Expand Down Expand Up @@ -52,6 +54,8 @@
}

$container->set(AssetsCompileEventListener::class);
$container->set(ServiceWorkerCompileEventListener::class);
$container->set(ServiceWorkerBuilder::class);

$container->set(PwaDevServerSubscriber::class)
->args([
Expand Down
5 changes: 5 additions & 0 deletions src/Resources/workbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
warmStrategyCache, // Warm the cache with URLs that are likely to be visited next or during offline navigation.
} = workbox.recipes;
const { CacheFirst } = workbox.strategies;
const { precacheAndRoute } = workbox.precaching;
const { registerRoute } = workbox.routing;
const { CacheableResponsePlugin } = workbox.cacheableResponse;
const { ExpirationPlugin } = workbox.expiration;
Expand Down Expand Up @@ -60,6 +61,10 @@ registerRoute(
}),
);

// This directive will be compiled and populated with asset routes and revisions
// At the moment, only static assets served by Asset Mapper are listed.
precacheAndRoute(self.__WB_MANIFEST);

// Warm the cache with URLs that are likely to be visited next or during offline navigation.
const strategy = new CacheFirst();
warmStrategyCache({urls: warmCacheUrls, strategy});
73 changes: 73 additions & 0 deletions src/Service/ServiceWorkerBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Service;

use SpomkyLabs\PwaBundle\Dto\Manifest;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\SerializerInterface;
use function assert;
use function is_string;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

final readonly class ServiceWorkerBuilder
{
private ?string $serviceworkerPublicUrl;

public function __construct(
private SerializerInterface $serializer,
private Manifest $manifest,
private AssetMapperInterface $assetMapper,
#[Autowire('%spomky_labs_pwa.serviceworker.precaching_placeholder%')]
private string $precachingPlaceholder,
) {
$serviceWorkerPublicUrl = $manifest->serviceWorker?->dest;
$this->serviceworkerPublicUrl = $serviceWorkerPublicUrl === null ? null : '/' . trim(
$serviceWorkerPublicUrl,
'/'
);
}

public function build(): ?string
{
if ($this->serviceworkerPublicUrl === null) {
return null;
}
$serviceWorkerSource = $this->manifest->serviceWorker?->src;
if ($serviceWorkerSource === null) {
return null;
}

if (! str_starts_with($serviceWorkerSource, '/')) {
$asset = $this->assetMapper->getAsset($serviceWorkerSource);
assert($asset !== null, 'Unable to find service worker source asset');
$body = $asset->content ?? file_get_contents($asset->sourcePath);
} else {
$body = file_get_contents($serviceWorkerSource);
}
assert(is_string($body), 'Unable to find service worker source content');
return $this->processPrecachedAssets($body);
}

private function processPrecachedAssets(string $body): string
{
if (! str_contains($body, $this->precachingPlaceholder)) {
return $body;
}
$result = [];
foreach ($this->assetMapper->allAssets() as $asset) {
$result[] = [
'url' => $asset->publicPath,
'revision' => $asset->digest,
];
}
return str_replace($this->precachingPlaceholder, $this->serializer->serialize($result, 'json', [
'json_encode_options' => JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
]), $body);
}
}
72 changes: 53 additions & 19 deletions src/Subscriber/PwaDevServerSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace SpomkyLabs\PwaBundle\Subscriber;

use SpomkyLabs\PwaBundle\Dto\Manifest;
use SpomkyLabs\PwaBundle\Service\ServiceWorkerBuilder;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -23,14 +24,22 @@
{
private string $manifestPublicUrl;

private null|string $serviceworkerPublicUrl;

public function __construct(
private ServiceWorkerBuilder $serviceWorkerBuilder,
private SerializerInterface $serializer,
private Manifest $manifest,
#[Autowire('%spomky_labs_pwa.manifest_public_url%')]
string $manifestPublicUrl,
private null|Profiler $profiler,
) {
$this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/');
$serviceWorkerPublicUrl = $manifest->serviceWorker?->dest;
$this->serviceworkerPublicUrl = $serviceWorkerPublicUrl === null ? null : '/' . trim(
$serviceWorkerPublicUrl,
'/'
);
}

public function onKernelRequest(RequestEvent $event): void
Expand All @@ -39,20 +48,44 @@ public function onKernelRequest(RequestEvent $event): void
return;
}

$pathInfo = $event->getRequest()
->getPathInfo();
if ($pathInfo !== $this->manifestPublicUrl) {
return;
//dd($event->getRequest()->getPathInfo(), $this->manifestPublicUrl, $this->serviceworkerPublicUrl);
switch ($event->getRequest()->getPathInfo()) {
case $this->manifestPublicUrl :
$this->serveManifest($event);
break;
case $this->serviceworkerPublicUrl :
$this->serveServiceWorker($event);
break;
}
}

public function onKernelResponse(ResponseEvent $event): void
{
$headers = $event->getResponse()
->headers;
if ($headers->has('X-Manifest-Dev') || $headers->has('X-SW-Dev')) {
$event->stopPropagation();
}
}

public static function getSubscribedEvents(): array
{
return [
// priority higher than RouterListener
KernelEvents::REQUEST => [['onKernelRequest', 35]],
// Highest priority possible to bypass all other listeners
KernelEvents::RESPONSE => [['onKernelResponse', 2048]],
];
}

private function serveManifest(RequestEvent $event): void
{
$this->profiler?->disable();
$body = $this->serializer->serialize($this->manifest, 'json', [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
'json_encode_options' => JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
]);

$this->profiler?->disable();

$response = new Response($body, Response::HTTP_OK, [
'Cache-Control' => 'public, max-age=604800, immutable',
'Content-Type' => 'application/manifest+json',
Expand All @@ -64,20 +97,21 @@ public function onKernelRequest(RequestEvent $event): void
$event->stopPropagation();
}

public function onKernelResponse(ResponseEvent $event): void
private function serveServiceWorker(RequestEvent $event): void
{
if ($event->getResponse()->headers->get('X-Manifest-Dev')) {
$event->stopPropagation();
$data = $this->serviceWorkerBuilder->build();
if ($data === null) {
return;
}
}
$this->profiler?->disable();

public static function getSubscribedEvents(): array
{
return [
// priority higher than RouterListener
KernelEvents::REQUEST => [['onKernelRequest', 35]],
// Highest priority possible to bypass all other listeners
KernelEvents::RESPONSE => [['onKernelResponse', 2048]],
];
$response = new Response($data, Response::HTTP_OK, [
'Content-Type' => 'application/manifest+json',
'X-SW-Dev' => true,
'Etag' => hash('xxh128', $data),
]);

$event->setResponse($response);
$event->stopPropagation();
}
}
39 changes: 39 additions & 0 deletions src/Subscriber/ServiceWorkerCompileEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Subscriber;

use SpomkyLabs\PwaBundle\Service\ServiceWorkerBuilder;
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
use Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(PreAssetsCompileEvent::class)]
final readonly class ServiceWorkerCompileEventListener
{
private ?string $serviceworkerPublicUrl;

public function __construct(
private ServiceWorkerBuilder $serviceWorkerBuilder,
#[Autowire('%spomky_labs_pwa.sw_public_url%')]
?string $serviceWorkerPublicUrl,
#[Autowire('@asset_mapper.local_public_assets_filesystem')]
private PublicAssetsFilesystemInterface $assetsFilesystem,
) {
$this->serviceworkerPublicUrl = $serviceWorkerPublicUrl === null ? null : '/' . trim(
$serviceWorkerPublicUrl,
'/'
);
}

public function __invoke(PreAssetsCompileEvent $event): void
{
$data = $this->serviceWorkerBuilder->build();
if ($data === null || $this->serviceworkerPublicUrl === null) {
return;
}
$this->assetsFilesystem->write($this->serviceworkerPublicUrl, $data);
}
}

0 comments on commit 55c981c

Please sign in to comment.