From af08d9f017877033d05975ac70c9e0e3780ba263 Mon Sep 17 00:00:00 2001 From: Sebastian Werner Date: Thu, 13 Jul 2023 20:19:52 +0200 Subject: [PATCH] feat: added svgo opt + internal tweaks --- .gitignore | 1 + package.json | 11 ++- pnpm-lock.yaml | 132 ++++++++++++++++++++++++++++ src/cli.ts | 2 +- src/index.ts | 80 ----------------- src/process.ts | 115 ++++++++++++++++++++++++ test/icon-sebastiansoftware-192.png | Bin 0 -> 780 bytes test/icon-sebastiansoftware-512.png | Bin 0 -> 1729 bytes test/icon-sebastiansoftware-ios.png | Bin 0 -> 876 bytes test/icon-sebastiansoftware-opt.svg | 1 + test/icon-sebastiansoftware.ico | Bin 0 -> 5430 bytes test/icon-sebastiansoftware.svg | 1 + tsconfig.json | 4 +- 13 files changed, 261 insertions(+), 86 deletions(-) delete mode 100644 src/index.ts create mode 100644 src/process.ts create mode 100644 test/icon-sebastiansoftware-192.png create mode 100644 test/icon-sebastiansoftware-512.png create mode 100644 test/icon-sebastiansoftware-ios.png create mode 100644 test/icon-sebastiansoftware-opt.svg create mode 100644 test/icon-sebastiansoftware.ico create mode 100644 test/icon-sebastiansoftware.svg diff --git a/.gitignore b/.gitignore index 1ab415f..f5d9e55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ *.tgz +.DS_Store diff --git a/package.json b/package.json index 8e09265..682aaeb 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,14 @@ "bin": { "effective-favicon": "dist/index.js" }, - "main": "dist/index.js", + "main": "dist/cli.js", "scripts": { "build": "rimraf dist && tsc", "prepare": "npm run build", "format": "prettier --write src", - "release": "release-it" + "release": "release-it", + "pretest": "npm run build", + "test": "node dist/cli.js test" }, "keywords": [ "icons", @@ -27,6 +29,7 @@ "url": "http://sebastian-software.de/werner" }, "license": "Apache-2.0", + "type": "module", "repository": { "type": "git", "url": "git+https://github.com/sebastian-software/effective-favicon.git" @@ -36,6 +39,7 @@ }, "devDependencies": { "@types/node": "^20.4.2", + "@types/pngquant-bin": "^4.0.0", "prettier": "^2.8.8", "release-it": "^15.11.0", "rimraf": "^5.0.1", @@ -45,6 +49,7 @@ "dependencies": { "png-to-ico": "^2.1.8", "pngquant-bin": "^8.0.1", - "sharp": "^0.32.2" + "sharp": "^0.32.2", + "svgo": "^3.0.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc92081..87eaf30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,11 +14,17 @@ dependencies: sharp: specifier: ^0.32.2 version: 0.32.2 + svgo: + specifier: ^3.0.2 + version: 3.0.2 devDependencies: '@types/node': specifier: ^20.4.2 version: 20.4.2 + '@types/pngquant-bin': + specifier: ^4.0.0 + version: 4.0.0 prettier: specifier: ^2.8.8 version: 2.8.8 @@ -261,6 +267,11 @@ packages: defer-to-connect: 2.0.1 dev: true + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: false + /@types/http-cache-semantics@4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: true @@ -278,6 +289,12 @@ packages: /@types/node@20.4.2: resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} + /@types/pngquant-bin@4.0.0: + resolution: {integrity: sha512-tCuGWs2eOJZxltxWci/NKGJUXVRR1tvweoE/TRNdsBy3WsscMbEFS4jCnS7axQAnWr+GFUeCmrHsGr2HhaWW1A==} + dependencies: + '@types/node': 20.4.2 + dev: true + /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: @@ -492,6 +509,10 @@ packages: readable-stream: 3.6.2 dev: true + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: false + /boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} @@ -742,6 +763,11 @@ packages: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: false + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -818,6 +844,44 @@ packages: type-fest: 1.4.0 dev: true + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.0.2 + dev: false + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: false + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: false + /data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -987,6 +1051,33 @@ packages: path-type: 4.0.0 dev: true + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + /dot-prop@6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} @@ -1051,6 +1142,11 @@ packages: once: 1.4.0 dev: false + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -2350,6 +2446,14 @@ packages: pify: 3.0.0 dev: false + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: false + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2533,6 +2637,12 @@ packages: dependencies: path-key: 4.0.0 + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2814,6 +2924,10 @@ packages: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: false + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: false + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -3377,6 +3491,11 @@ packages: is-plain-obj: 1.1.0 dev: false + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: false + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -3528,6 +3647,19 @@ packages: engines: {node: '>= 0.4'} dev: true + /svgo@3.0.2: + resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + csso: 5.0.5 + picocolors: 1.0.0 + dev: false + /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} dependencies: diff --git a/src/cli.ts b/src/cli.ts index 5265810..84f1d1c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,4 @@ -import { processSvgFiles } from './index'; +import { processSvgFiles } from "./process.js"; const args = process.argv.slice(2); if (args.length !== 1) { diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 1a4b0bc..0000000 --- a/src/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { promises as fs } from 'fs'; -import * as path from 'path'; -import * as svgo from 'svgo'; -import sharp from 'sharp'; -import ico from 'png-to-ico'; -import execa from 'execa'; -import pngquant from 'pngquant-bin'; - -const svgOptimize = new svgo(); - -const iosImageSize = 150; -const iosImagePadding = 15; // can be adjusted - -async function optimizePng(filePath: string) { - await execa(pngquant, ['--quality=60-80', '-o', filePath, filePath]); -} - -async function processSvgFile(filePath: string) { - try { - const fileName = path.basename(filePath, '.svg'); - const fileDir = path.dirname(filePath); - const svgString = await fs.readFile(filePath, 'utf-8'); - - const optimizedSvgString = (await svgOptimize.optimize(svgString)).data; - - // Save the optimized SVG with a "-opt" postfix - const optimizedSvgFilePath = `${fileDir}/${fileName}-opt.svg`; - await fs.writeFile(optimizedSvgFilePath, optimizedSvgString); - - // Generate PNG images at 32px, 192px, 512px and 150px - const sizes = [32, 192, 512, iosImageSize]; - for (const size of sizes) { - const pngFilePath = `${fileDir}/${fileName}-${size}.png`; - await sharp(Buffer.from(optimizedSvgString)).resize(size, size).png().toFile(pngFilePath); - - if (size === 32) { - // Create an icon (32px) - const buffer = await fs.readFile(pngFilePath); - const icoBuffer = await ico(buffer); - await fs.writeFile(`${fileDir}/${fileName}.ico`, icoBuffer); - } - - // Optimize the PNG - await optimizePng(pngFilePath); - } - - // Create a 180px png based on the 150px image and add padding - const imgIosPath = `${fileDir}/${fileName}-ios.png`; - await sharp(`${fileDir}/${fileName}-${iosImageSize}.png`) - .extend({ - top: iosImagePadding, - bottom: iosImagePadding, - left: iosImagePadding, - right: iosImagePadding, - background: { r: 0, g: 0, b: 0, alpha: 0 } - }) - .toFile(imgIosPath); - - // Optimize the 180px PNG - await optimizePng(imgIosPath); - } catch (err) { - console.error(`Error processing file ${filePath}: `, err); - } -} - -async function processSvgFiles(dirPath: string) { - const files = await fs.readdir(dirPath); - for (const file of files) { - const filePath = path.join(dirPath, file); - const stat = await fs.stat(filePath); - if (stat && stat.isDirectory()) { - processSvgFiles(filePath); - } else if (path.extname(file) === '.svg' && !file.endsWith('-opt.svg')) { - await processSvgFile(filePath); - } - } -} - -// Replace with your directory path -processSvgFiles('/path/to/your/svg/icons/directory').catch(console.error); diff --git a/src/process.ts b/src/process.ts new file mode 100644 index 0000000..28ca84e --- /dev/null +++ b/src/process.ts @@ -0,0 +1,115 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import {optimize} from 'svgo'; +import sharp from 'sharp'; +import ico from 'png-to-ico'; +import { exec } from 'child_process'; +import pngquant from "pngquant-bin" + +const MAX_IOS_IMAGE_SIZE = 180; +const IOS_IMAGE_PADDING = 15; +const IOS_PADDING_COLOR = { r: 0, g: 0, b: 0, alpha: 0 } +const PNG_QUALITY = "75-85" +const FAV_ICON_SIZES = [16, 32]; + + + +export async function optimizePng(filePath: string) { + return new Promise((resolve, reject) => { + const command = `${pngquant} --quality=${PNG_QUALITY} --force --output ${filePath} ${filePath}`; + exec(command, (error) => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); + }); +} + +export async function processSvgFile(filePath: string) { + try { + const fileName = path.basename(filePath, '.svg'); + const fileDir = path.dirname(filePath); + + const fileBase = path.join(fileDir, fileName); + + const svgString = await fs.readFile(filePath, 'utf-8'); + const optimizedSvgString = optimize(svgString).data; + + // Save the optimized SVG with a "-opt" postfix + const optimizedSvgFilePath = `${fileBase}-opt.svg`; + await fs.writeFile(optimizedSvgFilePath, optimizedSvgString); + + // Generate PNG images for various configured sizes + await generateBitmaps(fileBase, optimizedSvgString); + + // Generate favicon.ico + await generateClassicFavicon(fileBase); + + // Create a 180px IOS png based on a smaller scaled image with padding + await generateAppleTouchIcon(fileBase); + } catch (err) { + console.error(`Error processing file ${filePath}: `, err); + } +} + +async function generateBitmaps(fileBase: string, optimizedSvgString: string) { + const iosImageSize = MAX_IOS_IMAGE_SIZE - IOS_IMAGE_PADDING * 2; + const sizes = [...FAV_ICON_SIZES, 192, 512, iosImageSize]; + for (const size of sizes) { + const pngFilePath = `${fileBase}-${size}.png`; + await sharp(Buffer.from(optimizedSvgString)).resize(size, size).png().toFile(pngFilePath); + await optimizePng(pngFilePath); + } +} + +async function generateClassicFavicon(fileBase: string) { + const favBuffer = await Promise.all(FAV_ICON_SIZES.map((size) => { + const pngFilePath = `${fileBase}-${size}.png`; + return fs.readFile(pngFilePath); + })); + + // Create ICO FILE based on different source images + const icoBuffer = await ico(favBuffer); + await fs.writeFile(`${fileBase}.ico`, icoBuffer); + + // Delete intermediate PNG files + await Promise.all(FAV_ICON_SIZES.map((size) => { + const pngFilePath = `${fileBase}-${size}.png`; + return fs.rm(pngFilePath); + })); +} + +async function generateAppleTouchIcon(fileBase: string) { + const iosImageSize = MAX_IOS_IMAGE_SIZE - IOS_IMAGE_PADDING * 2; + const imgIosPath = `${fileBase}-ios.png`; + await sharp(`${fileBase}-${iosImageSize}.png`) + .extend({ + top: IOS_IMAGE_PADDING, + bottom: IOS_IMAGE_PADDING, + left: IOS_IMAGE_PADDING, + right: IOS_IMAGE_PADDING, + background: IOS_PADDING_COLOR + }) + .toFile(imgIosPath); + + // Delete temporary file + await fs.rm(`${fileBase}-${iosImageSize}.png`); + + // Optimize the 180px PNG + await optimizePng(imgIosPath); +} + +export async function processSvgFiles(dirPath: string) { + const files = await fs.readdir(dirPath); + for (const file of files) { + const filePath = path.join(dirPath, file); + const stat = await fs.stat(filePath); + if (stat && stat.isDirectory()) { + processSvgFiles(filePath); + } else if (path.extname(file) === '.svg' && !file.endsWith('-opt.svg')) { + await processSvgFile(filePath); + } + } +} diff --git a/test/icon-sebastiansoftware-192.png b/test/icon-sebastiansoftware-192.png new file mode 100644 index 0000000000000000000000000000000000000000..d7d9e85bfda9f7bcf7724926f195dc8729b350f1 GIT binary patch literal 780 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaM=0(?STU6c$OvrBGYz47PIpJTiC2N^lUD4PHO|Nn?wh=-c#Zn?lr-^foN zK9(rhZCJQ`{lew5J11u%Yrk0uU@5fQU=jJbaP+9MD@aX(S4@Cbz zZn~Er|6$huz=LP=V?V6=Uw80Ae)NZ3|Kkqc$dBA`Y=4Zw*W03NKI@B~`5VTX{r6$h zg!?NVB(Jp(5%@M+e`XK@Ndr_^}_$@yZ+l9ICj6T za^YM5(1hpy!3i(@0~20v*Wd6hTKvZET<)MBy4;t3yyXh|xt8nF&t9%m5%t0H$$!1; z_ifPhtT=or*Y!s|8nO4KNOwfCrtkz zCGhZl{)E~8w;woed*mh8r_bA2Z=Yxnc+0yl`Tl>EFZP?4oqYc_-t^D=yX+V-1B$D& z%l`ejFn70TclhA1IIAYxb|n5|f>OzVn^O@0{=a z&X;daigR)Db^-twlhLpUfZ$aEu!uh-sSOSMfmi7h^#I3g@{vq2e@9;}ii?J-ztz3` z#owHe5@XQn#>dAm`K0`HsXi?*e2sbe(BmgR`Nn^wPkhkbTk`$Ri*;wGZQS;hhn#OR z=u@(9K5Rex#e-Onm`s$(Kvf&giw`|=?Qp5WxNB-+^KC6^?ufJ7b~XR%n49cnDq3qQ z^KH>O?!X~g-%ht` zPbs^QZQt)h&v2!Rxc-NNCq%a4bd9uahp5oM`gAsPGz=n$j{f_CtA$K}3S}|Tq*Wj` zMkAr}DG$co8U~XI8O+0rAcgct6$+!MNmGbey+%q|sR;~WT>#_Q2}V>wXfTN-+40^V z!@Ea^OPOf!lH4*yq=zO!v*%Q=B8ArSp&eM~^DyaQt%cf=E~1pllOm<#Tbo zLT;B%+zb_d7Tn}&wV?_>MTQSOi0E*uib*9mdvNQCuBS-i3q|hPq9%#eCQ6IfoQdKb zBJN#c6)e+KOPH1T!mx<1GtL13S6?N|##qfr~7KgrP*3op-2 zN4G|uBX3JM+qor#{gvlPlA6j)7>CE$13u|afx;;)(#)xlFd@hj8cd7f5ZMF9f?q_M zBuYkYNDY8sVi{;Ju13Y~W|6KW)rETxMZl7~_mSP7B^@7U#6UL=L8UH7WRCsJbmnCK zVsys6No?_6>cz>?O1NCI2dT*{QJdRQ7rJIRQOzBQnv{bJ$+udTYG*F@4*0wbTnkr5 zQ@y#a+bw0aXy_fK&JN#xfD<6Zz}+Ki4ID|$!3DFZ`M426qPaezHkwmVD(pO)nuHTj z2*nK%wG=mz2}gJ3bTX%Ag3%K>oya0MjJ@qVkNbc&viWk_#46!a>|Mtbw-3<6&q&ufKR6~!kn_lT$hu2H~J1n4#ixznsSaY|dH_!t5TfT0e?ITo z4A}wZV?uN%+0w^1G8oOS78)rfTOM8=28+gACwO<_v8r1<7|-{?XVTn_h<+M5nc0&A z%v_iSUT?+xe@!kQK&7IfEkq~skY!8FT8A%XWpepBJHc=K^4}w~pkC1gg;QBVW1_^= z4#t(iXfmuL;^6?^4VTC)l&5GSbOEe{j;8+IdpyJs%#1C1O38(zL|Db|{uz~7JQz!P zax>6}5J;SY3^GU*=5Cf)G^`7qOikj-m@v@3NtV@(qZPWDkFr`S%Z108Vv-D1(e&1T E0H;tE#sB~S literal 0 HcmV?d00001 diff --git a/test/icon-sebastiansoftware-ios.png b/test/icon-sebastiansoftware-ios.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c3470b52a8a606dcb0fef1f81e1ffbe2b624f6 GIT binary patch literal 876 zcmV-y1C#uTP)Px#Bv4FLMMrQR0d!JMQvg8b*k%9#0@+DKK~!ko?b)$z6G0S(;qV4nw$@ST zMhcRZatqR?NI@IXKnD$?Oo_XQH;9m^W8nvg3!aa~IL14kaQ}0>?&g-3{p-`(JG-+! zIjK{pPMtb~&x_Gu^RlLEx~6NorpbpUk4H;iUmPnvxk-;lN?)gRah!CAPK0fwJ2YbJ z={1_L_4FD|*m8P<25dRKK?Am$-lEg%q0?J*`aE!Y2YowmT0o0w1udo(w3e38T3SL& zX$>uyw1K8+0}ay-8m1jIOIv7`&e4;r z>ly@&q#E_=tfJ2Rc5r}yW5_Gx?RrT5V(eea{&_RdQ0qf_d!PusS{j?*P{pWUHL z>^NOQ_t<~XC3cuDqj%YBbb%eF%jg~UUvz;TrJqCpV{g#=>?r*zKN0VT15+K70uH!nx|znPU~o#*3mR=plRAb!?c5jX$KA05C_fDZzHsQ>mH-O z+?wg|NH^(+myXe&irXiU2Iy2`$7zBt?)u_1LYLTKnxRYVFb&ZKc9f>*0y|1K6rIvX zF@1K^M~}wOpwsz-daRe()@94i(I4cmal@W%_laYO2EXfNwoTlyLo`UQ*|VKJ`Iw=} z8nVo`plM%BQ#pCm)YZwjt$T#efBeQ(-!2AYN5%RwE27#FX0000 \ No newline at end of file diff --git a/test/icon-sebastiansoftware.ico b/test/icon-sebastiansoftware.ico new file mode 100644 index 0000000000000000000000000000000000000000..5972aa5b6245200f6db7a0c26c13cb654d1ef9b2 GIT binary patch literal 5430 zcmZQzU}Ruo5D;Jh(h3Y2EDQ{43=9kk3K0GZpm-lpOamweWCB${#DNr6V4&pkpH|q; zFz`PJlWYFv3)lZ&yL^jmGqMsY|Nr{+`~UT;x9MUA$ge9FZTxSo>j(C;twPCv5Qe#f zkY8Zx&YrpiHp5eX-v3~^!zgBe)Zh!x(wt_npRt)iOk9A};4=fC-DxqUV7v2DYf#KE zQu6+f>=#^SfWi$F=b$))nNgnG0{5?rLic}=S-A9r#L>;brxuqSHOv5oBP{*mvWJj3 z$P8lQjF36_AnX%3x}QZF+3ke-NgIS)`f)viJM9 zA7D&xH<+q>{(tuL<$o9exr5$r@U)E{bpx^cCX&8A7lm<-|ga2D8WP#;q z5e`Y=h2Z@4?(GK*H-PN+m){I-htR?e=y?p?4ZiX#!FGe%K_HCF4WN7qGKQ`lVh16$1LX#gy}pid z;5ekW8$kAg(i|ua)7uT``48C*=xGu;k73IfATdJmi%lL@{=hOIvKwG=fh~_Ay8%`% zU^AZ>F?? \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index ca85730..1848de8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ // Create React App: // https://github.com/tsconfig/bases/blob/main/bases/create-react-app.json - "module": "esnext", + "module": "Node16", "allowJs": true, // To provide backwards compatibility, Node.js allows you to import most CommonJS packages with a default import. // This flag tells TypeScript that it's okay to use import on CommonJS modules. @@ -21,7 +21,7 @@ "isolatedModules": true, "jsx": "react-jsx", - "moduleResolution": "node", + "moduleResolution": "Node16", "noFallthroughCasesInSwitch": true, // Sindre Sorhus