Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better SW #36

Merged
merged 1 commit into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
71 changes: 52 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,43 @@ public function onKernelRequest(RequestEvent $event): void
return;
}

$pathInfo = $event->getRequest()
->getPathInfo();
if ($pathInfo !== $this->manifestPublicUrl) {
return;
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 +96,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);
}
}
Loading