diff --git a/.eslintignore b/.eslintignore index d652cf4e..f082429a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,6 +6,8 @@ *.txt Dockerfile public/ +public-dev/ +public-staging/ https-dev-config/localhost.crt https-dev-config/localhost.key Dockerfile diff --git a/config/pwa.ts b/config/pwa.ts index c6ce1016..19c82b7f 100644 --- a/config/pwa.ts +++ b/config/pwa.ts @@ -15,6 +15,16 @@ export const pwa: VitePWANuxtOptions = { injectManifest: { globPatterns: ['**/*.{js,json,css,html,txt,svg,png,ico,webp,woff,woff2,ttf,eot,otf,wasm}'], globIgnores: ['emojis/**', 'shiki/**', 'manifest**.webmanifest'], + manifestTransforms: [(entries) => { + const manifest = entries.map((entry) => { + if (entry.url.length > 1 && entry.url[0] !== '/') + entry.url = `/${entry.url}` + + return entry + }) + + return { manifest, warnings: [] } + }], }, devOptions: { enabled: process.env.VITE_DEV_PWA === 'true', diff --git a/docs/content/80.pwa.md b/docs/content/80.pwa.md index f2db0f5d..a610d7f2 100644 --- a/docs/content/80.pwa.md +++ b/docs/content/80.pwa.md @@ -103,6 +103,16 @@ Elk will generate 2 web manifests per locale, one for light theme and one for da You can check web manifest generation on [modules/pwa/i18n.ts](https://github.com/elk-zone/elk/blob/main/modules/pwa/i18n.ts) module. +### PWA Icons + +Elk's favicon and PWA icons are generated from [Elk's SVG Logo](https://github.com/elk-zone/elk/blob/main/public/logo.svg) via [custom script](https://github.com/elk-zone/elk/blob/main/scripts/generate-pwa-icons.ts), using [sharp](https://github.com/lovell/sharp/) and [sharp-io](https://github.com/ssnangua/sharp-ico) libraries: +- favicon.ico: transparent 64x64 32-bits icon +- pwa-64x64.png: transparent 64x64 8-bits icon (optimized from 32-bitss color) +- pwa-192x192.png: transparent 192x192 8-bits icon (optimized from 32-bits color) +- pwa-512x512.png: transparent 512x512 8-bits icon (optimized from 32-bit color) +- maskable-icon.png: white background 512x512 8-bits icon (optimized from 32-bits color) +- apple-touch-icon.png: white background 180x180 8-bits icon (optimized from 32-bits color) + ### PWA UI Components Elk will provide a set of UI components to allow you to customize the PWA installation prompt on browsers with [beforeinstallprompt](https://web.dev/customize-install/) support. diff --git a/modules/pwa/i18n.ts b/modules/pwa/i18n.ts index e52bdc8b..e45a3a45 100644 --- a/modules/pwa/i18n.ts +++ b/modules/pwa/i18n.ts @@ -76,8 +76,13 @@ export async function createI18n(): Promise { orientation: 'natural', display: 'standalone', display_override: ['window-controls-overlay'], - categories: ['social', 'social networking'], + categories: ['social', 'social networking', 'news'], icons: [ + { + src: 'pwa-64x64.png', + sizes: '64x64', + type: 'image/png', + }, { src: 'pwa-192x192.png', sizes: '192x192', @@ -114,6 +119,8 @@ export async function createI18n(): Promise { }, } + // TODO: add related_applications, only when env === 'release' + const locales: RequiredWebManifestEntry[] = await Promise.all( pwaLocales .filter(l => l.code !== 'en-US') diff --git a/package.json b/package.json index d69daa4b..b068632f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "update:team:avatars": "tsx scripts/avatars.ts", "cleanup-translations": "tsx scripts/cleanup-translations.ts", "prepare-translation-status": "tsx scripts/prepare-translation-status.ts", + "generate-pwa-icons": "tsx scripts/generate-pwa-icons.ts", "postinstall": "ignore-dependency-scripts \"stale-dep -u && simple-git-hooks && nuxi prepare && nr prepare-translation-status\"", "release": "stale-dep && bumpp && tsx scripts/release.ts" }, @@ -94,7 +95,7 @@ "ufo": "^1.1.2", "ultrahtml": "^1.2.0", "unimport": "^3.0.7", - "vite-plugin-pwa": "^0.15.0", + "vite-plugin-pwa": "^0.15.1", "vue-advanced-cropper": "^2.8.8", "vue-virtual-scroller": "2.0.0-beta.8", "workbox-build": "^6.5.4", @@ -120,6 +121,8 @@ "lint-staged": "^13.2.2", "nuxt": "3.5.2", "prettier": "^2.8.8", + "sharp": "^0.32.1", + "sharp-ico": "^0.1.5", "simple-git-hooks": "^2.8.1", "tsx": "^3.12.7", "typescript": "^5.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f898bd21..b5ce68e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,8 +205,8 @@ importers: specifier: ^3.0.7 version: 3.0.7(rollup@2.79.1) vite-plugin-pwa: - specifier: ^0.15.0 - version: 0.15.0(vite@4.3.9)(workbox-build@6.5.4)(workbox-window@6.5.4) + specifier: ^0.15.1 + version: 0.15.1(vite@4.3.9)(workbox-build@6.5.4)(workbox-window@6.5.4) vue-advanced-cropper: specifier: ^2.8.8 version: 2.8.8(vue@3.3.4) @@ -277,6 +277,12 @@ importers: prettier: specifier: ^2.8.8 version: 2.8.8 + sharp: + specifier: ^0.32.1 + version: 0.32.1 + sharp-ico: + specifier: ^0.1.5 + version: 0.1.5 simple-git-hooks: specifier: ^2.8.1 version: 2.8.1 @@ -1599,6 +1605,10 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 + /@canvas/image-data@1.0.0: + resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==} + dev: true + /@cloudflare/kv-asset-handler@0.3.0: resolution: {integrity: sha512-9CB/MKf/wdvbfkUdfrj+OkEwZ5b7rws0eogJ4293h+7b6KX5toPwym+VQKmILafNB9YiehqY0DlNrDcDhdWHSQ==} dependencies: @@ -5908,6 +5918,10 @@ packages: optionalDependencies: fsevents: 2.3.2 + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: true + /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -6023,10 +6037,25 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: true + /color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + /color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: true + /colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -6343,18 +6372,47 @@ packages: dependencies: ms: 2.1.2 + /decode-bmp@0.2.1: + resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==} + engines: {node: '>=8.6.0'} + dependencies: + '@canvas/image-data': 1.0.0 + to-data-view: 1.1.0 + dev: true + + /decode-ico@0.4.1: + resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==} + engines: {node: '>=8.6'} + dependencies: + '@canvas/image-data': 1.0.0 + decode-bmp: 0.2.1 + to-data-view: 1.1.0 + dev: true + /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} dependencies: character-entities: 2.0.2 dev: true + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} dependencies: type-detect: 4.0.8 + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -7418,6 +7476,11 @@ packages: signal-exit: 3.0.7 strip-final-newline: 3.0.0 + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: true + /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: true @@ -7811,6 +7874,10 @@ packages: dependencies: git-up: 7.0.0 + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: true + /github-reserved-names@2.0.4: resolution: {integrity: sha512-T2azXbRJTJGQc28G6x89LpzQmuVjzl0hzJXPRD2t9yMh7URYUW8Opqr5ptHvjAVDJ+hwhBtoYmVx3VyFawRoFg==} dev: false @@ -8194,6 +8261,10 @@ packages: ms: 2.1.3 dev: false + /ico-endec@0.1.6: + resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==} + dev: true + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -8377,6 +8448,10 @@ packages: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: true + /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} dependencies: @@ -9724,6 +9799,11 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -9839,6 +9919,10 @@ packages: resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==} dev: false + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: true + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -9917,6 +10001,10 @@ packages: engines: {node: ^14 || ^16 || >=18} hasBin: true + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: true + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -10019,6 +10107,17 @@ packages: lower-case: 2.0.2 tslib: 2.5.2 + /node-abi@3.40.0: + resolution: {integrity: sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.1 + dev: true + + /node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + dev: true + /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -11301,6 +11400,25 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.1 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.40.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -11521,6 +11639,13 @@ packages: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: false + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -11554,6 +11679,16 @@ packages: destr: 1.2.2 flat: 5.0.2 + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true + /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -12153,6 +12288,29 @@ packages: /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + /sharp-ico@0.1.5: + resolution: {integrity: sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q==} + dependencies: + decode-ico: 0.4.1 + ico-endec: 0.1.6 + sharp: 0.32.1 + dev: true + + /sharp@0.32.1: + resolution: {integrity: sha512-kQTFtj7ldpUqSe8kDxoGLZc1rnMFU0AO2pqbX6pLy3b7Oj8ivJIdoKNwxHVQG2HN6XpHPJqCSM2nsma2gOXvOg==} + engines: {node: '>=14.15.0'} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.1 + node-addon-api: 6.1.0 + prebuild-install: 7.1.1 + semver: 7.5.1 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -12213,6 +12371,18 @@ packages: - supports-color dev: false + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: true + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: true + /simple-git-hooks@2.8.1: resolution: {integrity: sha512-DYpcVR1AGtSfFUNzlBdHrQGPsOhuuEJ/FkmPOOlFysP60AHd3nsEpkGq/QEOdtUyT1Qhk7w9oLmFoMG+75BDog==} hasBin: true @@ -12229,6 +12399,12 @@ packages: - supports-color dev: false + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: true + /sirv@2.0.3: resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} engines: {node: '>= 10'} @@ -12574,6 +12750,11 @@ packages: min-indent: 1.0.1 dev: true + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -12671,6 +12852,15 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: true + /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -12819,6 +13009,10 @@ packages: dependencies: os-tmpdir: 1.0.2 + /to-data-view@1.1.0: + resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==} + dev: true + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -12910,6 +13104,12 @@ packages: - supports-color dev: false + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -13698,8 +13898,8 @@ packages: - supports-color dev: false - /vite-plugin-pwa@0.15.0(vite@4.3.9)(workbox-build@6.5.4)(workbox-window@6.5.4): - resolution: {integrity: sha512-gpmx3BeubsRIXRBkjPToOTJbo8fknNmZFQs24i0TPZyaNVa0n27YHDo0Y72amnO70WvHKGE3e1fn8SYUP7e8SA==} + /vite-plugin-pwa@0.15.1(vite@4.3.9)(workbox-build@6.5.4)(workbox-window@6.5.4): + resolution: {integrity: sha512-lJVzEYda/Y9AfwxFzX0rV+QCQ2+WdBoEGtR1RBZKWxvrJ4NWEH1VZaHOMyzvRiYhWQsi7aFhewsp1CDvN/R1Og==} peerDependencies: vite: ^3.1.0 || ^4.0.0 workbox-build: ^6.5.4 diff --git a/public-dev/apple-touch-icon.png b/public-dev/apple-touch-icon.png index 2334d36d..8e43d9a3 100644 Binary files a/public-dev/apple-touch-icon.png and b/public-dev/apple-touch-icon.png differ diff --git a/public-dev/favicon.ico b/public-dev/favicon.ico index 39829a08..7d3e8038 100644 Binary files a/public-dev/favicon.ico and b/public-dev/favicon.ico differ diff --git a/public-dev/logo.svg b/public-dev/logo.svg index e8415d88..16426876 100644 --- a/public-dev/logo.svg +++ b/public-dev/logo.svg @@ -18,7 +18,7 @@ maskUnits="userSpaceOnUse" style="mask-type:alpha"> diff --git a/public-dev/maskable-icon.png b/public-dev/maskable-icon.png index 82864824..277ad084 100644 Binary files a/public-dev/maskable-icon.png and b/public-dev/maskable-icon.png differ diff --git a/public-dev/pwa-192x192.png b/public-dev/pwa-192x192.png index fd6c0ed3..35296812 100644 Binary files a/public-dev/pwa-192x192.png and b/public-dev/pwa-192x192.png differ diff --git a/public-dev/pwa-512x512.png b/public-dev/pwa-512x512.png index 609b9d15..ed9560e3 100644 Binary files a/public-dev/pwa-512x512.png and b/public-dev/pwa-512x512.png differ diff --git a/public-dev/pwa-64x64.png b/public-dev/pwa-64x64.png new file mode 100644 index 00000000..c3c0f1e4 Binary files /dev/null and b/public-dev/pwa-64x64.png differ diff --git a/public-staging/apple-touch-icon.png b/public-staging/apple-touch-icon.png index 0d640e80..46a0fa76 100644 Binary files a/public-staging/apple-touch-icon.png and b/public-staging/apple-touch-icon.png differ diff --git a/public-staging/favicon.ico b/public-staging/favicon.ico index bf186fc9..1d2cb016 100644 Binary files a/public-staging/favicon.ico and b/public-staging/favicon.ico differ diff --git a/public-staging/logo.svg b/public-staging/logo.svg index 01dad652..cbcb7c48 100644 --- a/public-staging/logo.svg +++ b/public-staging/logo.svg @@ -18,7 +18,7 @@ maskUnits="userSpaceOnUse" style="mask-type:alpha"> diff --git a/public-staging/maskable-icon.png b/public-staging/maskable-icon.png index 0f6560a3..901e5100 100644 Binary files a/public-staging/maskable-icon.png and b/public-staging/maskable-icon.png differ diff --git a/public-staging/pwa-192x192.png b/public-staging/pwa-192x192.png index 2416d6e9..d12180b7 100644 Binary files a/public-staging/pwa-192x192.png and b/public-staging/pwa-192x192.png differ diff --git a/public-staging/pwa-512x512.png b/public-staging/pwa-512x512.png index 661be7e3..e08371c9 100644 Binary files a/public-staging/pwa-512x512.png and b/public-staging/pwa-512x512.png differ diff --git a/public-staging/pwa-64x64.png b/public-staging/pwa-64x64.png new file mode 100644 index 00000000..caaf1c76 Binary files /dev/null and b/public-staging/pwa-64x64.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 8efbf415..93217ec0 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 563520c2..9621b7bd 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/logo.svg b/public/logo.svg index aea8c9d9..3645460f 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -18,7 +18,7 @@ maskUnits="userSpaceOnUse" style="mask-type:alpha"> diff --git a/public/maskable-icon.png b/public/maskable-icon.png index 63518cca..64ac7ac8 100644 Binary files a/public/maskable-icon.png and b/public/maskable-icon.png differ diff --git a/public/pwa-192x192.png b/public/pwa-192x192.png index e7e81d6d..98ff658c 100644 Binary files a/public/pwa-192x192.png and b/public/pwa-192x192.png differ diff --git a/public/pwa-512x512.png b/public/pwa-512x512.png index fe911849..9a201728 100644 Binary files a/public/pwa-512x512.png and b/public/pwa-512x512.png differ diff --git a/public/pwa-64x64.png b/public/pwa-64x64.png new file mode 100644 index 00000000..f3c91cef Binary files /dev/null and b/public/pwa-64x64.png differ diff --git a/scripts/generate-pwa-icons.ts b/scripts/generate-pwa-icons.ts new file mode 100644 index 00000000..c98282e5 --- /dev/null +++ b/scripts/generate-pwa-icons.ts @@ -0,0 +1,208 @@ +import { rm, writeFile } from 'node:fs/promises' +import { resolve } from 'pathe' +import type { PngOptions, ResizeOptions } from 'sharp' +import sharp from 'sharp' +import ico from 'sharp-ico' + +interface Icon { + sizes: number[] + padding: number + resizeOptions?: ResizeOptions +} + +type IconType = 'transparent' | 'maskable' | 'apple' + +/** + * PWA Icons definition: + * - transparent: [{ sizes: [192, 512], padding: 0.05, resizeOptions: { fit: 'contain', background: 'transparent' } }] + * - maskable: [{ sizes: [512], padding: 0.3 }, resizeOptions: { fit: 'contain', background: 'white' } }] + * - apple: [{ sizes: [180], padding: 0.3 }, resizeOptions: { fit: 'contain', background: 'white' } }] + */ +interface Icons extends Record { + /** + * @default: { compressionLevel: 9, quality: 60 }` + */ + png?: PngOptions + /** + * @default `pwa-x.png`, `maskable-icon-x.png`, `apple-touch-icon-x.png` + */ + iconName?: (type: IconType, size: number) => string + /** + * Generate `favicon.ico` from transparent icons (from `pwa-x.png` ones) + */ + ico?: { + /** + * @default `favicon-x.ico` + */ + icoName?: (size: number) => string + sizes: number[] + } +} + +interface ResolvedIcons extends Required> { + ico?: { + /** + * @default `favicon-x.ico` + */ + icoName?: (size: number) => string + sizes: number[] + } +} + +const defaultIcons: Icons = { + transparent: { + sizes: [192, 512], + padding: 0.05, + resizeOptions: { + fit: 'contain', + background: 'transparent', + }, + }, + maskable: { + sizes: [512], + padding: 0.3, + resizeOptions: { + fit: 'contain', + background: 'white', + }, + }, + apple: { + sizes: [180], + padding: 0.3, + resizeOptions: { + fit: 'contain', + background: 'white', + }, + }, +} + +const root = process.cwd() + +const publicFolders = ['public', 'public-dev', 'public-staging'].map(folder => resolve(root, folder)) + +async function optimizePng(filePath: string, png: PngOptions) { + await sharp(filePath).png(png).toFile(`${filePath.replace(/-temp\.png$/, '.png')}`) + await rm(filePath) +} + +async function generateTransparentIcons(icons: ResolvedIcons, svgLogo: string, folder: string) { + const { sizes, padding, resizeOptions } = icons.transparent + await Promise.all(sizes.map(async (size) => { + const filePath = resolve(folder, icons.iconName('transparent', size)) + await sharp({ + create: { + width: size, + height: size, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }, + }).composite([{ + input: await sharp(svgLogo) + .resize( + Math.round(size * (1 - padding)), + Math.round(size * (1 - padding)), + resizeOptions, + ).toBuffer(), + }]).toFile(filePath) + await optimizePng(filePath, icons.png) + })) +} + +async function generateMaskableIcons(type: IconType, icons: ResolvedIcons, svgLogo: string, folder: string) { + const { sizes, padding, resizeOptions } = icons[type] + await Promise.all(sizes.map(async (size) => { + const filePath = resolve(folder, icons.iconName(type, size)) + await sharp({ + create: { + width: size, + height: size, + channels: 4, + background: resizeOptions?.background ?? 'white', + }, + }).composite([{ + input: await sharp(svgLogo) + .resize( + Math.round(size * (1 - padding)), + Math.round(size * (1 - padding)), + resizeOptions, + ).toBuffer(), + }]).toFile(filePath) + await optimizePng(filePath, icons.png) + })) +} + +async function generatePWAIconForEnv(folder: string, icons: ResolvedIcons) { + const svgLogo = resolve(folder, 'logo.svg') + await Promise.all([ + generateTransparentIcons(icons, svgLogo, folder), + generateMaskableIcons('maskable', icons, svgLogo, folder), + generateMaskableIcons('apple', icons, svgLogo, folder), + ]) + + if (icons.ico) { + const { + icoName = size => `favicon-${size}x${size}.ico`, + } = icons.ico + await Promise.all(icons.ico.sizes.map(async (size) => { + const png = await sharp( + resolve(folder, icons.iconName('transparent', size).replace(/-temp\.png$/, '.png')), + ).toFormat('png').toBuffer() + await writeFile(resolve(folder, icoName(size)), ico.encode([png])) + })) + } +} + +async function generatePWAIcons(folders: string[], icons: Icons) { + const { + png = { compressionLevel: 9, quality: 60 }, + iconName = (type, size) => { + switch (type) { + case 'transparent': + return `pwa-${size}x${size}.png` + case 'maskable': + return `maskable-icon-${size}x${size}.png` + case 'apple': + return `apple-touch-icon-${size}x${size}.png` + } + }, + transparent = { ...defaultIcons.transparent }, + maskable = { ...defaultIcons.maskable }, + apple = { ...defaultIcons.apple }, + ico, + } = icons + + if (!transparent.resizeOptions) + transparent.resizeOptions = { ...defaultIcons.transparent.resizeOptions } + + if (!maskable.resizeOptions) + maskable.resizeOptions = { ...defaultIcons.maskable.resizeOptions } + + if (!apple.resizeOptions) + apple.resizeOptions = { ...defaultIcons.apple.resizeOptions } + + await Promise.all(folders.map(folder => generatePWAIconForEnv(folder, { + png, + iconName, + transparent, + maskable, + apple, + ico, + }))) +} + +console.log('Generating Elk PWA Icons...') + +generatePWAIcons(publicFolders, { + transparent: { ...defaultIcons.transparent, sizes: [64, 192, 512] }, + ico: { sizes: [64], icoName: _ => 'favicon.ico' }, + iconName: (type, size) => { + switch (type) { + case 'transparent': + return `pwa-${size}x${size}-temp.png` + case 'maskable': + return 'maskable-icon-temp.png' + case 'apple': + return 'apple-touch-icon-temp.png' + } + }, +}).then(() => console.log('Elk PWA Icons generated')).catch(console.error)