From bdb2cb3817ab8dc895bb8f87866236ebd363f6c0 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Mon, 18 Dec 2023 21:33:56 +0100 Subject: [PATCH 1/3] Update README and implement Workbox Service Worker Improved structure and clarity in README for better understanding of manifest configuration. Also added implementation of a Workbox-based Service Worker, providing offline capabilities and push notifications, improving overall robustness of the web application. --- README.md | 62 ++++++++++++++++++--- src/Command/GenerateManifestCommand.php | 4 +- src/Command/WorkboxInitCommand.php | 72 +++++++++++++++++++++++++ src/Resources/config/services.php | 2 + src/Resources/workbox.js | 65 ++++++++++++++++++++++ 5 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 src/Command/WorkboxInitCommand.php create mode 100644 src/Resources/workbox.js diff --git a/README.md b/README.md index f1fb3ce..d7e663e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,14 @@ This project follows the [semantic versioning](http://semver.org/) strictly. # Documentation -## Manifest Configuration +A Progressive Web Application (PWA) is a web application that has a manifest and can be installed on a device. +The manifest is a JSON file that describes the application and its assets (icons, screenshots, etc.). + +A Service Worker can be used to provide offline capabilities to the application or to provide push notifications. + +## Manifest + +### Manifest Configuration The bundle is able to generate a manifest from a configuration file. The manifest members defined in the [Web app manifests](https://developer.mozilla.org/en-US/docs/Web/Manifest) are supported @@ -57,7 +64,7 @@ phpwa: 'application/json': ['.json'] ``` -## Manifest Generation +### Manifest Generation When the configuration is set, you can generate the manifest with the following command: @@ -98,7 +105,7 @@ For instance, if your application root URL is https://example.com/foo/bar, set t } ``` -## Manifest Icons +### Manifest Icons The bundle is able to generate icons from a source image. The icons must be square and the source image should be at best quality as possible. @@ -109,7 +116,7 @@ Depending on your system, you may have to install one extension or the other. ```yaml # config/packages/phpwa.yaml phpwa: - icon_processor: 'pwa.icon_processor.gd' # or 'pwa.icon_processor.imagick' + image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick' icons: - src: "%kernel.project_dir%/assets/images/logo.png" sizes: [48, 57, 60, 72, 76, 96, 114, 128, 144, 152, 180, 192, 256, 384, 512, 1024] @@ -126,7 +133,7 @@ With the configuration above, the bundle will generate * 16 icons from the `mask.png` image. The format will be `png` and the purpose will be `maskable`. * And 1 icon from the `logo.svg` image. The format will be `svg` and the size will be `any`. -## Manifest Screenshots +### Manifest Screenshots The bundle is able to generate screenshots from a source image. Similar to icons, the source image should be at best quality as possible. @@ -134,6 +141,7 @@ Similar to icons, the source image should be at best quality as possible. ```yaml # config/packages/phpwa.yaml phpwa: + image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick' screenshots: - src: "%kernel.project_dir%/assets/screenshots/narrow.png" label: "View of the application home page" @@ -149,7 +157,7 @@ The bundle will automatically generate screenshots from the source images and ad such as the `sizes` and the `form_factor` (`wide` or `narrow`). The `format` parameter is optional. It indicates the format of the generated image. If not set, the format will be the same as the source image. -## Manifest Shortcuts +### Manifest Shortcuts The `shortcuts` member may contain a list of icons. The parameters are very similar to the `icons` member. @@ -157,6 +165,7 @@ The parameters are very similar to the `icons` member. ```yaml # config/packages/phpwa.yaml phpwa: + image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick' shortcuts: - name: "Shortcut 1" short_name: "shortcut-1" @@ -176,6 +185,47 @@ phpwa: format: 'webp' ``` +### Using the Manifest + +The manifest can be used in your HTML pages with the following code in the `` section. +In you customized the output filename or the public folder, please replace `pwa.json` with the path to your manifest file. + +```html + +``` + +## Service Worker + +The following command will generate a Service Worker in the `public` directory. + +```bash +symfony console pwa:sw +``` + +You can change the output file name and the output folder with the following options: + +* `--output` or `-o` to change the output file name (default: `sw.js`) +* `--public_folder` or `-p` to change the public folder (default: `%kernel.project_dir%/public`) + +Next, you have to register the Service Worker in your HTML pages with the following code in the `` section. +It can also be done in a JavaScript file such as `app.js`. +In you customized the output filename or the public folder, please replace `sw.js` with the path to your Service Worker file. + +```html + +``` + +### Service Worker Configuration + +The Service Worker uses Workbox and comes with predefined configuration and recipes. +You are free to change the configuration and the recipes to fit your needs. +In particular, you can change the cache strategy, the cache expiration, the cache name, etc. # Support diff --git a/src/Command/GenerateManifestCommand.php b/src/Command/GenerateManifestCommand.php index f5f553e..d8c3cea 100644 --- a/src/Command/GenerateManifestCommand.php +++ b/src/Command/GenerateManifestCommand.php @@ -31,17 +31,15 @@ final class GenerateManifestCommand extends Command { private readonly MimeTypes $mime; - private readonly Filesystem $filesystem; - public function __construct( private readonly null|ImageProcessor $imageProcessor, #[Autowire('%spomky_labs_pwa.config%')] private readonly array $config, #[Autowire('%kernel.project_dir%')] private readonly string $rootDir, + private readonly Filesystem $filesystem, ) { $this->mime = MimeTypes::getDefault(); - $this->filesystem = new Filesystem(); parent::__construct(); } diff --git a/src/Command/WorkboxInitCommand.php b/src/Command/WorkboxInitCommand.php new file mode 100644 index 0000000..a83df96 --- /dev/null +++ b/src/Command/WorkboxInitCommand.php @@ -0,0 +1,72 @@ +addOption( + 'public_folder', + 'p', + InputOption::VALUE_OPTIONAL, + 'Public folder', + $this->rootDir . '/public' + ) + ->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Output file', 'sw.js') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title('Workbox Service Worker'); + + $publicFolder = Path::canonicalize($input->getOption('public_folder')); + $outputFile = '/' . trim((string) $input->getOption('output'), '/'); + + if (! $this->filesystem->exists($publicFolder)) { + $this->filesystem->mkdir($publicFolder); + } + + $resourcePath = $this->fileLocator->locate('@SpomkyLabsPwaBundle/Resources/workbox.js', null, false); + if (count($resourcePath) !== 1) { + $io->error('Unable to find the Workbox resource.'); + return self::FAILURE; + } + $resourcePath = $resourcePath[0]; + $this->filesystem->copy($resourcePath, $publicFolder . $outputFile); + + + $io->success('Workbox is ready to use!'); + + return self::SUCCESS; + } +} diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 4b2f8ef..27df479 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -3,6 +3,7 @@ declare(strict_types=1); use SpomkyLabs\PwaBundle\Command\GenerateManifestCommand; +use SpomkyLabs\PwaBundle\Command\WorkboxInitCommand; use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor; use SpomkyLabs\PwaBundle\ImageProcessor\ImagickImageProcessor; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -16,6 +17,7 @@ ; $container->set(GenerateManifestCommand::class); + $container->set(WorkboxInitCommand::class); if (extension_loaded('imagick')) { $container diff --git a/src/Resources/workbox.js b/src/Resources/workbox.js new file mode 100644 index 0000000..fbb9f6d --- /dev/null +++ b/src/Resources/workbox.js @@ -0,0 +1,65 @@ +importScripts( + 'https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js' +); + +const { + pageCache, // Cache pages with a network-first strategy. + imageCache, // Cache images with a cache-first strategy. + staticResourceCache, // Cache CSS, JS, and Web Worker requests with a cache-first strategy for 1 year. + offlineFallback, // Serve an offline fallback page when the user is offline and try to revalidate the request when the user is online. + warmStrategyCache, // Warm the cache with URLs that are likely to be visited next or during offline navigation. +} = workbox.recipes; +const { CacheFirst } = workbox.strategies; +const { registerRoute } = workbox.routing; +const { CacheableResponsePlugin } = workbox.cacheableResponse; +const { ExpirationPlugin } = workbox.expiration; + +const PAGE_CACHE_NAME = 'pages'; +const FONT_CACHE_NAME = 'fonts'; +const STATIC_CACHE_NAME = 'assets'; +const IMAGE_CACHE_NAME = 'images'; +const OFFLINE_URI = '/offline'; // URI of the offline fallback page. +const warmCacheUrls = [ // URLs to warm the cache with. + '/', +]; + +// *** Recipes *** +// Cache pages with a network-first strategy. +pageCache({ + cacheName: PAGE_CACHE_NAME +}); +// Cache CSS, JS, and Web Worker requests with a cache-first strategy. +staticResourceCache({ + cacheName: STATIC_CACHE_NAME, +}); +// Cache images with a cache-first strategy. +imageCache({ + cacheName: IMAGE_CACHE_NAME, + maxEntries: 60, // Default 60 images + maxAgeSeconds: 60 * 60 * 24 * 30, // Default 30 days +}); +// Serve an offline fallback page when the user is offline and try to revalidate the request when the user is online. +offlineFallback({ + pageFallback: OFFLINE_URI, +}); + +// Cache the underlying font files with a cache-first strategy. +registerRoute( + ({request}) => request.destination === 'font', + new CacheFirst({ + cacheName: FONT_CACHE_NAME, + plugins: [ + new CacheableResponsePlugin({ + statuses: [0, 200], + }), + new ExpirationPlugin({ + maxAgeSeconds: 60 * 60 * 24 * 365, + maxEntries: 30, + }), + ], + }), +); + +// Warm the cache with URLs that are likely to be visited next or during offline navigation. +const strategy = new CacheFirst(); +warmStrategyCache({urls: warmCacheUrls, strategy}); From e9eba2b9a7747a229e5f772caa0e932b112efba6 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Mon, 18 Dec 2023 21:34:09 +0100 Subject: [PATCH 2/3] Add link script and update .gitattributes Added a script named 'link' to help automate linking project dependencies. .gitattributes has also been updated with new file paths. This enhancement will facilitate and streamline the project setup process by automating preparation tasks. --- .gitattributes | 11 ++++---- link | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 link diff --git a/.gitattributes b/.gitattributes index 90c4a5d..91d3c46 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,14 +1,15 @@ * text=auto /.github export-ignore -/doc export-ignore /tests export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.php_cs.dist export-ignore -/.scrutinizer.yml export-ignore -/.travis.yml export-ignore /CODE_OF_CONDUCT.md export-ignore -/README.md export-ignore +/ecs.php export-ignore +/infection.json.dist export-ignore +/link export-ignore +/Makefile export-ignore /phpstan.neon export-ignore +/phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore +/rector export-ignore diff --git a/link b/link new file mode 100644 index 0000000..e25523c --- /dev/null +++ b/link @@ -0,0 +1,68 @@ +#!/usr/bin/env php +exists($composer = "$dir/composer.json")) { + $packages[json_decode(file_get_contents($composer))->name] = $dir; + } +} + +if (is_dir("$pathToProject/vendor/spomky-labs/phpwa")) { + if ($filesystem->exists($composer = "$pathToProject/vendor/spomky-labs/phpwa/composer.json")) { + $packages[json_decode(file_get_contents($composer))->name] = realpath(__DIR__); + } +} + +foreach (glob("$pathToProject/vendor/spomky-labs/*", GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { + $package = 'spomky-labs/'.basename($dir); + if (!$copy && is_link($dir)) { + echo "\"$package\" is already a symlink, skipping.".PHP_EOL; + continue; + } + + if (!isset($packages[$package])) { + continue; + } + + $packageDir = ('\\' === DIRECTORY_SEPARATOR || $copy) ? $packages[$package] : $filesystem->makePathRelative($packages[$package], dirname(realpath($dir))); + $filesystem->remove($dir); + + if ($copy) { + $filesystem->mirror($packageDir, $dir); + echo "\"$package\" has been copied from \"$packages[$package]\".".PHP_EOL; + } else { + $filesystem->symlink(rtrim($packageDir, '/'), $dir); + echo "\"$package\" has been linked to \"$packages[$package]\".".PHP_EOL; + } +} From 8f8f86001f89d3aec2486fe0b18ad0d0c8be897a Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Mon, 18 Dec 2023 21:34:36 +0100 Subject: [PATCH 3/3] Refactor WorkboxInitCommand class and remove extra line Simplified the annotation and constructor on the `WorkboxInitCommand` class to enhance readability. In addition, removed a superfluous empty line. This refactor contributes to cleaner code and easier maintenance. --- src/Command/WorkboxInitCommand.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Command/WorkboxInitCommand.php b/src/Command/WorkboxInitCommand.php index a83df96..9607199 100644 --- a/src/Command/WorkboxInitCommand.php +++ b/src/Command/WorkboxInitCommand.php @@ -14,11 +14,9 @@ use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Path; use Symfony\Component\HttpKernel\Config\FileLocator; +use function count; -#[AsCommand( - name: 'pwa:sw', - description: 'Initializes the Workbox-based Service Worker.', -)] +#[AsCommand(name: 'pwa:sw', description: 'Initializes the Workbox-based Service Worker.',)] class WorkboxInitCommand extends Command { public function __construct( @@ -64,7 +62,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $resourcePath = $resourcePath[0]; $this->filesystem->copy($resourcePath, $publicFolder . $outputFile); - $io->success('Workbox is ready to use!'); return self::SUCCESS;