From a0fe3cd35edb244339ea7fa6eb6cb843ba642b60 Mon Sep 17 00:00:00 2001 From: thecodrr Date: Thu, 5 Aug 2021 10:02:56 +0500 Subject: [PATCH] feat: add zoom factor & home page settings --- apps/web/desktop/{ => config}/theme.js | 0 apps/web/desktop/config/zoomfactor.js | 12 +++++++ apps/web/desktop/electron.js | 6 ++-- .../web/desktop/ipc/actions/changeAppTheme.js | 16 ++++----- .../web/desktop/ipc/actions/checkForUpdate.js | 7 ++-- .../web/desktop/ipc/actions/downloadUpdate.js | 11 +++---- apps/web/desktop/ipc/actions/index.js | 20 +++++++++-- apps/web/desktop/ipc/actions/installUpdate.js | 7 ++-- apps/web/desktop/ipc/actions/openLink.js | 9 ++--- apps/web/desktop/ipc/actions/saveFile.js | 33 +++++++++---------- apps/web/desktop/ipc/actions/setZoomFactor.js | 8 +++++ apps/web/desktop/ipc/calls/getZoomFactor.js | 5 +++ apps/web/desktop/ipc/calls/index.js | 14 ++++++++ apps/web/desktop/ipc/index.js | 12 +++++-- apps/web/desktop/package.json | 1 + apps/web/desktop/preload.js | 11 ++++++- apps/web/desktop/yarn.lock | 14 ++++++++ apps/web/package.json | 1 + apps/web/src/commands/set-zoom-factor.js | 5 +++ apps/web/src/common/checkout.js | 4 +-- apps/web/src/components/dialogs/buydialog.js | 1 - apps/web/src/hooks/use-zoom-factor.js | 20 +++++++++++ apps/web/src/index.js | 25 +++++++++----- apps/web/src/views/settings.js | 31 +++++++++++++++++ apps/web/yarn.lock | 25 ++++++++++++++ 25 files changed, 229 insertions(+), 69 deletions(-) rename apps/web/desktop/{ => config}/theme.js (100%) create mode 100644 apps/web/desktop/config/zoomfactor.js create mode 100644 apps/web/desktop/ipc/actions/setZoomFactor.js create mode 100644 apps/web/desktop/ipc/calls/getZoomFactor.js create mode 100644 apps/web/desktop/ipc/calls/index.js create mode 100644 apps/web/src/commands/set-zoom-factor.js create mode 100644 apps/web/src/hooks/use-zoom-factor.js diff --git a/apps/web/desktop/theme.js b/apps/web/desktop/config/theme.js similarity index 100% rename from apps/web/desktop/theme.js rename to apps/web/desktop/config/theme.js diff --git a/apps/web/desktop/config/zoomfactor.js b/apps/web/desktop/config/zoomfactor.js new file mode 100644 index 000000000..0be610c6d --- /dev/null +++ b/apps/web/desktop/config/zoomfactor.js @@ -0,0 +1,12 @@ +const storage = require("electron-data-storage").default; + +function getZoomFactor() { + let factor = parseFloat(storage.getSync("zoomFactor")); + return isNaN(factor) ? 1.0 : factor; +} + +function setZoomFactor(factor) { + return storage.set("zoomFactor", factor.toString()); +} + +module.exports = { setZoomFactor, getZoomFactor }; diff --git a/apps/web/desktop/electron.js b/apps/web/desktop/electron.js index 9f1bcea88..bbaa774be 100644 --- a/apps/web/desktop/electron.js +++ b/apps/web/desktop/electron.js @@ -4,7 +4,8 @@ const os = require("os"); const { isDevelopment } = require("./utils"); const { registerProtocol, URL } = require("./protocol"); const { configureAutoUpdater } = require("./autoupdate"); -const { getTheme, getBackgroundColor } = require("./theme"); +const { getBackgroundColor } = require("./config/theme"); +const getZoomFactor = require("./ipc/calls/getZoomFactor"); require("./ipc/index.js"); try { require("electron-reloader")(module); @@ -21,11 +22,12 @@ async function createWindow() { os.platform() === "win32" ? "app.ico" : "favicon-72x72.png" ), webPreferences: { + zoomFactor: getZoomFactor(), devTools: true, // isDev, nodeIntegration: false, //true, enableRemoteModule: false, contextIsolation: true, - sandbox: true, + // sandbox: true, preload: __dirname + "/preload.js", }, }); diff --git a/apps/web/desktop/ipc/actions/changeAppTheme.js b/apps/web/desktop/ipc/actions/changeAppTheme.js index b90dbb67d..deda293b4 100644 --- a/apps/web/desktop/ipc/actions/changeAppTheme.js +++ b/apps/web/desktop/ipc/actions/changeAppTheme.js @@ -1,12 +1,8 @@ -const { setTheme, getBackgroundColor } = require("../../theme"); +const { setTheme, getBackgroundColor } = require("../../config/theme"); -module.exports = { - type: "changeAppTheme", - action: async (args) => { - console.log("args", args); - if (!global.win) return; - const { theme } = args; - await setTheme(theme); - global.win.setBackgroundColor(getBackgroundColor()); - }, +module.exports = async (args) => { + if (!global.win) return; + const { theme } = args; + await setTheme(theme); + global.win.setBackgroundColor(getBackgroundColor()); }; diff --git a/apps/web/desktop/ipc/actions/checkForUpdate.js b/apps/web/desktop/ipc/actions/checkForUpdate.js index b1d10f17c..1a68d3c4f 100644 --- a/apps/web/desktop/ipc/actions/checkForUpdate.js +++ b/apps/web/desktop/ipc/actions/checkForUpdate.js @@ -1,8 +1,5 @@ const { autoUpdater } = require("electron-updater"); -module.exports = { - type: "checkForUpdate", - action: () => { - autoUpdater.checkForUpdates(); - }, +module.exports = () => { + autoUpdater.checkForUpdates(); }; diff --git a/apps/web/desktop/ipc/actions/downloadUpdate.js b/apps/web/desktop/ipc/actions/downloadUpdate.js index 4426e2b8b..e4d2f6c0c 100644 --- a/apps/web/desktop/ipc/actions/downloadUpdate.js +++ b/apps/web/desktop/ipc/actions/downloadUpdate.js @@ -3,11 +3,8 @@ const { autoUpdater } = require("electron-updater"); const { sendMessageToRenderer } = require(".."); const { EVENTS } = require("../../events"); -module.exports = { - type: "downloadUpdate", - action: () => { - sendMessageToRenderer(EVENTS.updateDownloadProgress, { progress: 0 }); - autoUpdater.cancellationToken = new CancellationToken(); - autoUpdater.downloadUpdate(autoUpdater.cancellationToken); - }, +module.exports = () => { + sendMessageToRenderer(EVENTS.updateDownloadProgress, { progress: 0 }); + autoUpdater.cancellationToken = new CancellationToken(); + autoUpdater.downloadUpdate(autoUpdater.cancellationToken); }; diff --git a/apps/web/desktop/ipc/actions/index.js b/apps/web/desktop/ipc/actions/index.js index aadca3e17..0f206c2c4 100644 --- a/apps/web/desktop/ipc/actions/index.js +++ b/apps/web/desktop/ipc/actions/index.js @@ -1,8 +1,24 @@ -const actions = {}; +const changeAppTheme = require("./changeAppTheme"); +const checkForUpdate = require("./checkForUpdate"); +const downloadUpdate = require("./downloadUpdate"); +const installUpdate = require("./installUpdate"); +const openLink = require("./openLink"); +const saveFile = require("./saveFile"); +const setZoomFactor = require("./setZoomFactor"); + +const actions = { + changeAppTheme, + checkForUpdate, + downloadUpdate, + installUpdate, + openLink, + saveFile, + setZoomFactor, +}; module.exports.getAction = function getAction(actionName) { try { - if (!actions[actionName]) actions[actionName] = require(`./${actionName}`); + if (!actions[actionName]) throw new Error("Invalid action name."); } catch (e) { console.error(e); } diff --git a/apps/web/desktop/ipc/actions/installUpdate.js b/apps/web/desktop/ipc/actions/installUpdate.js index 55b66f6a5..84d56dda1 100644 --- a/apps/web/desktop/ipc/actions/installUpdate.js +++ b/apps/web/desktop/ipc/actions/installUpdate.js @@ -1,8 +1,5 @@ const { autoUpdater } = require("electron-updater"); -module.exports = { - type: "installUpdate", - action: () => { - autoUpdater.quitAndInstall(); - }, +module.exports = () => { + autoUpdater.quitAndInstall(); }; diff --git a/apps/web/desktop/ipc/actions/openLink.js b/apps/web/desktop/ipc/actions/openLink.js index 5a7679e72..990e939ca 100644 --- a/apps/web/desktop/ipc/actions/openLink.js +++ b/apps/web/desktop/ipc/actions/openLink.js @@ -1,8 +1,5 @@ const { shell } = require("electron"); -module.exports = { - type: "openLink", - action: (args) => { - const { link } = args; - return shell.openExternal(link); - }, +module.exports = (args) => { + const { link } = args; + return shell.openExternal(link); }; diff --git a/apps/web/desktop/ipc/actions/saveFile.js b/apps/web/desktop/ipc/actions/saveFile.js index 56523eb00..eb1426729 100644 --- a/apps/web/desktop/ipc/actions/saveFile.js +++ b/apps/web/desktop/ipc/actions/saveFile.js @@ -2,22 +2,19 @@ const { app } = require("electron"); const fs = require("fs"); const path = require("path"); -module.exports = { - type: "saveFile", - action: (args) => { - const { data, filePath } = args; - if (!data || !filePath) return; - const resolvedPath = path.join( - ...filePath.split("/").map((segment) => { - let resolved = segment; - try { - resolved = app.getPath(resolved); - } finally { - return resolved; - } - }) - ); - fs.mkdirSync(path.dirname(resolvedPath), { recursive: true }); - fs.writeFileSync(resolvedPath, data); - }, +module.exports = (args) => { + const { data, filePath } = args; + if (!data || !filePath) return; + const resolvedPath = path.join( + ...filePath.split("/").map((segment) => { + let resolved = segment; + try { + resolved = app.getPath(resolved); + } finally { + return resolved; + } + }) + ); + fs.mkdirSync(path.dirname(resolvedPath), { recursive: true }); + fs.writeFileSync(resolvedPath, data); }; diff --git a/apps/web/desktop/ipc/actions/setZoomFactor.js b/apps/web/desktop/ipc/actions/setZoomFactor.js new file mode 100644 index 000000000..9be9ff321 --- /dev/null +++ b/apps/web/desktop/ipc/actions/setZoomFactor.js @@ -0,0 +1,8 @@ +const { setZoomFactor } = require("../../config/zoomfactor"); + +module.exports = async (args) => { + if (!global.win) return; + const { zoomFactor } = args; + global.win.webContents.setZoomFactor(zoomFactor); + await setZoomFactor(zoomFactor); +}; diff --git a/apps/web/desktop/ipc/calls/getZoomFactor.js b/apps/web/desktop/ipc/calls/getZoomFactor.js new file mode 100644 index 000000000..6bc0f4d4f --- /dev/null +++ b/apps/web/desktop/ipc/calls/getZoomFactor.js @@ -0,0 +1,5 @@ +const { getZoomFactor } = require("../../config/zoomfactor"); + +module.exports = function () { + return getZoomFactor(); +}; diff --git a/apps/web/desktop/ipc/calls/index.js b/apps/web/desktop/ipc/calls/index.js new file mode 100644 index 000000000..4788cb3db --- /dev/null +++ b/apps/web/desktop/ipc/calls/index.js @@ -0,0 +1,14 @@ +const getZoomFactor = require("./getZoomFactor"); + +const calls = { + getZoomFactor, +}; + +module.exports.getCall = function getAction(callName) { + try { + if (!calls[callName]) throw new Error("Invalid call name."); + } catch (e) { + console.error(e); + } + return calls[callName]; +}; diff --git a/apps/web/desktop/ipc/index.js b/apps/web/desktop/ipc/index.js index 582068fe4..0c101cb3c 100644 --- a/apps/web/desktop/ipc/index.js +++ b/apps/web/desktop/ipc/index.js @@ -1,11 +1,19 @@ -const { ipcMain } = require("electron"); +const { ipcMain } = require("electron-better-ipc"); const { getAction } = require("./actions"); +const { getCall } = require("./calls"); ipcMain.on("fromRenderer", async (event, args) => { const { type } = args; const action = getAction(type); if (!action) return; - await action.action(args); + await action(args); +}); + +ipcMain.answerRenderer("fromRenderer", async (args, win) => { + const { type } = args; + const call = getCall(type); + if (!call) return; + return await call(args, win); }); module.exports.sendMessageToRenderer = function (type, payload = {}) { diff --git a/apps/web/desktop/package.json b/apps/web/desktop/package.json index 56ffd9379..d4b886717 100644 --- a/apps/web/desktop/package.json +++ b/apps/web/desktop/package.json @@ -8,6 +8,7 @@ "homepage": "https://notesnook.com/", "repository": "https://github.com/streetwriters/notesnook", "dependencies": { + "electron-better-ipc": "^2.0.1", "electron-data-storage": "^1.0.7", "electron-serve": "^1.1.0", "electron-updater": "^4.3.8", diff --git a/apps/web/desktop/preload.js b/apps/web/desktop/preload.js index 0c7c8d8c6..5895c3b15 100644 --- a/apps/web/desktop/preload.js +++ b/apps/web/desktop/preload.js @@ -1,4 +1,5 @@ -const { contextBridge, ipcRenderer } = require("electron"); +const { contextBridge } = require("electron"); +const { ipcRenderer } = require("electron-better-ipc"); // Expose protected methods that allow the renderer process to use // the ipcRenderer without exposing the entire object @@ -20,3 +21,11 @@ contextBridge.exposeInMainWorld("api", { } }, }); + +// Expose protected methods that allow the renderer process to use +// the ipcRenderer without exposing the entire object +contextBridge.exposeInMainWorld("config", { + zoomFactor: () => { + return ipcRenderer.callMain("fromRenderer", { type: "getZoomFactor" }); + }, +}); diff --git a/apps/web/desktop/yarn.lock b/apps/web/desktop/yarn.lock index 799be521c..96084ce9c 100644 --- a/apps/web/desktop/yarn.lock +++ b/apps/web/desktop/yarn.lock @@ -776,6 +776,13 @@ ejs@^3.1.6: dependencies: jake "^10.6.1" +electron-better-ipc@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/electron-better-ipc/-/electron-better-ipc-2.0.1.tgz#982a4468e814f9f19308c7865a148c6dca5e218a" + integrity sha512-S/h2vjQjev9FVicGnZeuP2wH/ycO9pOv63JGaTjYB2m5JcKDuXHAM713YmnmDC+VVXP7mh1Y9rtxi6BoNK7YJA== + dependencies: + serialize-error "^8.1.0" + electron-builder-notarize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.2.0.tgz#6db86173601513bcb667074f80322f8622e24ff9" @@ -1934,6 +1941,13 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +serialize-error@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" diff --git a/apps/web/package.json b/apps/web/package.json index 7b2210656..5c6a36ee1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,6 +26,7 @@ "highlight.js": "^11.1.0", "hotkeys-js": "^3.8.3", "immer": "^8.0.1", + "just-debounce": "^1.1.0", "localforage": "^1.7.3", "localforage-getitems": "https://github.com/thecodrr/localForage-getItems.git", "notes-core": "npm:@streetwriters/notesnook-core@latest", diff --git a/apps/web/src/commands/set-zoom-factor.js b/apps/web/src/commands/set-zoom-factor.js new file mode 100644 index 000000000..d7a1dd870 --- /dev/null +++ b/apps/web/src/commands/set-zoom-factor.js @@ -0,0 +1,5 @@ +import { invokeCommand } from "./index"; + +export default function setZoomFactor(zoomFactor) { + invokeCommand("setZoomFactor", { zoomFactor }); +} diff --git a/apps/web/src/common/checkout.js b/apps/web/src/common/checkout.js index 1555c7763..31753e224 100644 --- a/apps/web/src/common/checkout.js +++ b/apps/web/src/common/checkout.js @@ -67,8 +67,8 @@ async function openPaddleDialog(overrideUrl) { async function getCouponData(coupon, plan) { let url = plan === "monthly" - ? "https://checkout-service.paddle.com/checkout/97379638-chre9f4b00ed6b3-732b0e853d/coupon" - : "https://checkout-service.paddle.com/checkout/98675996-chre252e5763eaf-15b142d0e3/coupon"; + ? "https://checkout-service.paddle.com/checkout/104179806-chre3cd9a1fe123-92d5147457/coupon" + : "https://checkout-service.paddle.com/checkout/104179637-chre46de2d16625-e24512470d/coupon"; const response = await fetch(url, { headers: { diff --git a/apps/web/src/components/dialogs/buydialog.js b/apps/web/src/components/dialogs/buydialog.js index e88f30573..5a8303772 100644 --- a/apps/web/src/components/dialogs/buydialog.js +++ b/apps/web/src/components/dialogs/buydialog.js @@ -123,7 +123,6 @@ function BuyDialog(props) { setIsLoading(true); setError(); const data = await getCouponData(coupon, plan); - console.log(data); setPrices({ ...data.paddlejs.vendor, withoutDiscount: data.total[0], diff --git a/apps/web/src/hooks/use-zoom-factor.js b/apps/web/src/hooks/use-zoom-factor.js new file mode 100644 index 000000000..9c2672cc8 --- /dev/null +++ b/apps/web/src/hooks/use-zoom-factor.js @@ -0,0 +1,20 @@ +import { useCallback, useEffect, useState } from "react"; +import setZoomFactor from "../commands/set-zoom-factor"; + +export default function useZoomFactor() { + const [zoom, setZoom] = useState(1.0); + + useEffect(() => { + if (!window.config) return; + (async function () { + setZoom(await window.config.zoomFactor()); + })(); + }, []); + + const set = useCallback((zoomFactor) => { + setZoomFactor(zoomFactor); + setZoom(zoomFactor); + }, []); + + return [zoom, set]; +} diff --git a/apps/web/src/index.js b/apps/web/src/index.js index ae1021a0e..76a72d3da 100644 --- a/apps/web/src/index.js +++ b/apps/web/src/index.js @@ -12,15 +12,24 @@ if (process.env.NODE_ENV === "production") { console.log = () => {}; } -initializeDatabase().then(async (db) => { +const HOMEPAGE_ROUTE = { 1: "/notebooks", 2: "/favorites", 3: "/tags" }; + +async function checkRedirects(db) { const isLoggedIn = !!(await db.user.getUser()); - if ( - !process.env.REACT_APP_CI && - !isLoggedIn && - window.location.pathname === "/" && - !Config.get("skipInitiation", false) - ) - window.location.replace("/signup"); + if (window.location.pathname === "/") { + const skipInitiation = Config.get("skipInitiation", false); + const homepage = Config.get("homepage", 0); + if (!process.env.REACT_APP_CI && !isLoggedIn && !skipInitiation) + window.location.replace("/signup"); + else if (homepage) { + const route = HOMEPAGE_ROUTE[homepage]; + window.location.replace(route); + } + } +} + +initializeDatabase().then(async (db) => { + await checkRedirects(db); import("react-dom").then(({ render }) => { import("./App").then(({ default: App }) => { diff --git a/apps/web/src/views/settings.js b/apps/web/src/views/settings.js index 89f224414..f05d3d384 100644 --- a/apps/web/src/views/settings.js +++ b/apps/web/src/views/settings.js @@ -31,6 +31,9 @@ import openLink from "../commands/openLink"; import { isDesktop } from "../utils/platform"; import Vault from "../common/vault"; import { isUserPremium } from "../hooks/use-is-user-premium"; +import { Slider } from "@rebass/forms"; +import useZoomFactor from "../hooks/use-zoom-factor"; +import debounce from "just-debounce"; function importBackup() { return new Promise((resolve, reject) => { @@ -131,6 +134,7 @@ function Settings(props) { const toggleNightMode = useThemeStore((store) => store.toggleNightMode); const setTheme = useThemeStore((store) => store.setTheme); const followSystemTheme = useThemeStore((store) => store.followSystemTheme); + const [zoomFactor, setZoomFactor] = useZoomFactor(); const [, version] = useVersion(); const toggleFollowSystemTheme = useThemeStore( @@ -146,6 +150,7 @@ function Settings(props) { "backupReminderOffset", 0 ); + const [homepage, setHomepage] = usePersistentState("homepage", 0); const [enableTelemetry, setEnableTelemetry] = usePersistentState( "telemetry", true @@ -332,6 +337,32 @@ function Settings(props) { onToggled={toggleFollowSystemTheme} isToggled={followSystemTheme} /> + setHomepage(index)} + /> + {isDesktop && ( + <> + + { + setZoomFactor(e.target.valueAsNumber); + }, 500)} + /> + + )} )} diff --git a/apps/web/yarn.lock b/apps/web/yarn.lock index d2dc6cdde..b13bc10dc 100644 --- a/apps/web/yarn.lock +++ b/apps/web/yarn.lock @@ -1926,6 +1926,7 @@ "@notesnook/desktop@./desktop/": version "1.4.1" dependencies: + electron-better-ipc "^2.0.1" electron-data-storage "^1.0.7" electron-serve "^1.1.0" electron-updater "^4.3.8" @@ -5089,6 +5090,13 @@ ejs@^3.1.5: dependencies: jake "^10.6.1" +electron-better-ipc@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/electron-better-ipc/-/electron-better-ipc-2.0.1.tgz#982a4468e814f9f19308c7865a148c6dca5e218a" + integrity sha512-S/h2vjQjev9FVicGnZeuP2wH/ycO9pOv63JGaTjYB2m5JcKDuXHAM713YmnmDC+VVXP7mh1Y9rtxi6BoNK7YJA== + dependencies: + serialize-error "^8.1.0" + electron-data-storage@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/electron-data-storage/-/electron-data-storage-1.0.7.tgz#983322d44f9af0bb6f5c9cb66eccc291e14ff486" @@ -7850,6 +7858,11 @@ jsprim@^1.2.2: array-includes "^3.1.2" object.assign "^4.1.2" +just-debounce@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" + integrity sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ== + killable@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz" @@ -11051,6 +11064,13 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" +serialize-error@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" @@ -12088,6 +12108,11 @@ type-fest@^0.11.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz"