Merge pull request #1371 from hydralauncher/feature/adding-sentry

Feature/adding sentry
This commit is contained in:
Zamitto 2025-01-01 19:48:17 -03:00 committed by GitHub
commit 5c2bafcfe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 178 additions and 62 deletions

View File

@ -47,13 +47,13 @@
"auto-launch": "^5.0.6", "auto-launch": "^5.0.6",
"axios": "^1.7.9", "axios": "^1.7.9",
"better-sqlite3": "^11.7.0", "better-sqlite3": "^11.7.0",
"check-disk-space": "^3.4.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"color": "^4.2.3", "color": "^4.2.3",
"color.js": "^1.2.0", "color.js": "^1.2.0",
"create-desktop-shortcuts": "^1.11.0", "create-desktop-shortcuts": "^1.11.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dexie": "^4.0.10", "dexie": "^4.0.10",
"diskusage": "^1.2.0",
"electron-log": "^5.2.4", "electron-log": "^5.2.4",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"file-type": "^19.6.0", "file-type": "^19.6.0",

View File

@ -178,7 +178,8 @@
"select_folder": "Select folder", "select_folder": "Select folder",
"backup_from": "Backup from {{date}}", "backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set", "custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected" "no_directory_selected": "No directory selected",
"no_write_permission": "Cannot download into this directory. Click here to learn more."
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View File

@ -167,7 +167,8 @@
"select_folder": "Selecione a pasta", "select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup", "manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar", "clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado" "no_directory_selected": "Nenhum diretório selecionado",
"no_write_permission": "O download não pode ser feito neste diretório. Clique aqui para saber mais."
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",

View File

@ -0,0 +1,15 @@
import fs from "node:fs";
import { registerEvent } from "../register-event";
const checkFolderWritePermission = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) =>
new Promise((resolve) => {
fs.access(path, fs.constants.W_OK, (err) => {
resolve(!err);
});
});
registerEvent("checkFolderWritePermission", checkFolderWritePermission);

View File

@ -1,10 +1,10 @@
import checkDiskSpace from "check-disk-space"; import disk from "diskusage";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
const getDiskFreeSpace = async ( const getDiskFreeSpace = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
path: string path: string
) => checkDiskSpace(path); ) => disk.check(path);
registerEvent("getDiskFreeSpace", getDiskFreeSpace); registerEvent("getDiskFreeSpace", getDiskFreeSpace);

View File

@ -11,6 +11,7 @@ import "./catalogue/get-trending-games";
import "./catalogue/get-publishers"; import "./catalogue/get-publishers";
import "./catalogue/get-developers"; import "./catalogue/get-developers";
import "./hardware/get-disk-free-space"; import "./hardware/get-disk-free-space";
import "./hardware/check-folder-write-permission";
import "./library/add-game-to-library"; import "./library/add-game-to-library";
import "./library/create-game-shortcut"; import "./library/create-game-shortcut";
import "./library/close-game"; import "./library/close-game";
@ -30,6 +31,8 @@ import "./library/select-game-wine-prefix";
import "./misc/open-checkout"; import "./misc/open-checkout";
import "./misc/open-external"; import "./misc/open-external";
import "./misc/show-open-dialog"; import "./misc/show-open-dialog";
import "./misc/get-features";
import "./misc/show-item-in-folder";
import "./torrenting/cancel-game-download"; import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download"; import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download"; import "./torrenting/resume-game-download";
@ -71,7 +74,6 @@ import "./cloud-save/delete-game-artifact";
import "./cloud-save/select-game-backup-path"; import "./cloud-save/select-game-backup-path";
import "./notifications/publish-new-repacks-notification"; import "./notifications/publish-new-repacks-notification";
import { isPortableVersion } from "@main/helpers"; import { isPortableVersion } from "@main/helpers";
import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion); ipcMain.handle("getVersion", () => appVersion);

View File

@ -0,0 +1,8 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
};
registerEvent("getFeatures", getFeatures);

View File

@ -64,7 +64,10 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders( this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => { (details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) { if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot")
) {
return callback(details); return callback(details);
} }
@ -81,15 +84,11 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onHeadersReceived( this.mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => { (details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) { if (
return callback(details); details.webContentsId !== this.mainWindow?.webContents.id ||
} details.url.includes("featurebase") ||
details.url.includes("chatwoot")
if (details.url.includes("featurebase")) { ) {
return callback(details);
}
if (details.url.includes("chatwoot")) {
return callback(details); return callback(details);
} }

View File

@ -150,6 +150,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => getDiskFreeSpace: (path: string) =>
ipcRenderer.invoke("getDiskFreeSpace", path), ipcRenderer.invoke("getDiskFreeSpace", path),
checkFolderWritePermission: (path: string) =>
ipcRenderer.invoke("checkFolderWritePermission", path),
/* Cloud save */ /* Cloud save */
uploadSaveGame: ( uploadSaveGame: (
@ -226,6 +228,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showOpenDialog", options), ipcRenderer.invoke("showOpenDialog", options),
showItemInFolder: (path: string) => showItemInFolder: (path: string) =>
ipcRenderer.invoke("showItemInFolder", path), ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
platform: process.platform, platform: process.platform,
/* Auto update */ /* Auto update */

View File

@ -46,6 +46,12 @@ export function Modal({
}, [onClose]); }, [onClose]);
const isTopMostModal = () => { const isTopMostModal = () => {
if (
document.querySelector(
".featurebase-widget-overlay.featurebase-display-block"
)
)
return false;
const openModals = document.querySelectorAll("[role=dialog]"); const openModals = document.querySelectorAll("[role=dialog]");
return ( return (

View File

@ -1,6 +1,5 @@
import React, { useId, useMemo, useState } from "react"; import React, { useId, useMemo, useState } from "react";
import type { RecipeVariants } from "@vanilla-extract/recipes"; import type { RecipeVariants } from "@vanilla-extract/recipes";
import type { FieldError, FieldErrorsImpl, Merge } from "react-hook-form";
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react"; import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -23,7 +22,7 @@ export interface TextFieldProps
HTMLDivElement HTMLDivElement
>; >;
rightContent?: React.ReactNode | null; rightContent?: React.ReactNode | null;
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined; error?: string | React.ReactNode;
} }
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>( export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
@ -55,10 +54,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
}, [props.type, isPasswordVisible]); }, [props.type, isPasswordVisible]);
const hintContent = useMemo(() => { const hintContent = useMemo(() => {
if (error && error.message) if (error) return <small className={styles.errorLabel}>{error}</small>;
return (
<small className={styles.errorLabel}>{error.message as string}</small>
);
if (hint) return <small>{hint}</small>; if (hint) return <small>{hint}</small>;
return null; return null;

View File

@ -31,7 +31,7 @@ import type {
CatalogueSearchPayload, CatalogueSearchPayload,
} from "@types"; } from "@types";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import type { DiskSpace } from "check-disk-space"; import type disk from "diskusage";
declare global { declare global {
declare module "*.svg" { declare module "*.svg" {
@ -140,7 +140,8 @@ declare global {
) => Promise<{ fingerprint: string }>; ) => Promise<{ fingerprint: string }>;
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskSpace>; getDiskFreeSpace: (path: string) => Promise<disk.DiskUsage>;
checkFolderWritePermission: (path: string) => Promise<boolean>;
/* Cloud save */ /* Cloud save */
uploadSaveGame: ( uploadSaveGame: (
@ -195,6 +196,7 @@ declare global {
options: Electron.OpenDialogOptions options: Electron.OpenDialogOptions
) => Promise<Electron.OpenDialogReturnValue>; ) => Promise<Electron.OpenDialogReturnValue>;
showItemInFolder: (path: string) => Promise<void>; showItemInFolder: (path: string) => Promise<void>;
getFeatures: () => Promise<string[]>;
platform: NodeJS.Platform; platform: NodeJS.Platform;
/* Auto update */ /* Auto update */

View File

@ -6,3 +6,4 @@ export * from "./redux";
export * from "./use-user-details"; export * from "./use-user-details";
export * from "./use-format"; export * from "./use-format";
export * from "./use-repacks"; export * from "./use-repacks";
export * from "./use-feature";

View File

@ -0,0 +1,23 @@
import { useEffect } from "react";
enum Feature {
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
}
export function useFeature() {
useEffect(() => {
window.electron.getFeatures().then((features) => {
localStorage.setItem("features", JSON.stringify(features || []));
});
}, []);
const isFeatureEnabled = (feature: Feature) => {
const features = JSON.parse(localStorage.getItem("features") || "[]");
return features.includes(feature);
};
return {
isFeatureEnabled,
Feature,
};
}

View File

@ -13,6 +13,7 @@ import type {
UpdateProfileRequest, UpdateProfileRequest,
UserDetails, UserDetails,
} from "@types"; } from "@types";
import * as Sentry from "@sentry/react";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { isFuture, isToday } from "date-fns"; import { isFuture, isToday } from "date-fns";
@ -30,6 +31,8 @@ export function useUserDetails() {
} = useAppSelector((state) => state.userDetails); } = useAppSelector((state) => state.userDetails);
const clearUserDetails = useCallback(async () => { const clearUserDetails = useCallback(async () => {
Sentry.setUser(null);
dispatch(setUserDetails(null)); dispatch(setUserDetails(null));
dispatch(setProfileBackground(null)); dispatch(setProfileBackground(null));
@ -44,6 +47,12 @@ export function useUserDetails() {
const updateUserDetails = useCallback( const updateUserDetails = useCallback(
async (userDetails: UserDetails) => { async (userDetails: UserDetails) => {
Sentry.setUser({
id: userDetails.id,
username: userDetails.username,
email: userDetails.email ?? undefined,
});
dispatch(setUserDetails(userDetails)); dispatch(setUserDetails(userDetails));
window.localStorage.setItem("userDetails", JSON.stringify(userDetails)); window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
}, },

View File

@ -36,3 +36,10 @@ export const downloaderIcon = style({
position: "absolute", position: "absolute",
left: `${SPACING_UNIT * 2}px`, left: `${SPACING_UNIT * 2}px`,
}); });
export const pathError = style({
cursor: "pointer",
":hover": {
textDecoration: "underline",
},
});

View File

@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { DiskSpace } from "check-disk-space";
import * as styles from "./download-settings-modal.css"; import * as styles from "./download-settings-modal.css";
import { Button, Link, Modal, TextField } from "@renderer/components"; import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
@ -10,7 +9,7 @@ import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
import type { GameRepack } from "@types"; import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { DOWNLOADER_NAME } from "@renderer/constants"; import { DOWNLOADER_NAME } from "@renderer/constants";
import { useAppSelector, useToast } from "@renderer/hooks"; import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
export interface DownloadSettingsModalProps { export interface DownloadSettingsModalProps {
visible: boolean; visible: boolean;
@ -33,21 +32,45 @@ export function DownloadSettingsModal({
const { showErrorToast } = useToast(); const { showErrorToast } = useToast();
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null); const [diskFreeSpace, setDiskFreeSpace] = useState<number | null>(null);
const [selectedPath, setSelectedPath] = useState(""); const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false); const [downloadStarting, setDownloadStarting] = useState(false);
const [selectedDownloader, setSelectedDownloader] = const [selectedDownloader, setSelectedDownloader] =
useState<Downloader | null>(null); useState<Downloader | null>(null);
const [hasWritePermission, setHasWritePermission] = useState<boolean | null>(
null
);
const { isFeatureEnabled, Feature } = useFeature();
const userPreferences = useAppSelector( const userPreferences = useAppSelector(
(state) => state.userPreferences.value (state) => state.userPreferences.value
); );
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
setDiskFreeSpace(result.free);
});
};
const checkFolderWritePermission = useCallback(
async (path: string) => {
if (isFeatureEnabled(Feature.CheckDownloadWritePermission)) {
const result = await window.electron.checkFolderWritePermission(path);
setHasWritePermission(result);
} else {
setHasWritePermission(true);
}
},
[Feature, isFeatureEnabled]
);
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
getDiskFreeSpace(selectedPath); getDiskFreeSpace(selectedPath);
checkFolderWritePermission(selectedPath);
} }
}, [visible, selectedPath]); }, [visible, checkFolderWritePermission, selectedPath]);
const downloaders = useMemo(() => { const downloaders = useMemo(() => {
return getDownloadersForUris(repack?.uris ?? []); return getDownloadersForUris(repack?.uris ?? []);
@ -84,12 +107,6 @@ export function DownloadSettingsModal({
userPreferences?.realDebridApiToken, userPreferences?.realDebridApiToken,
]); ]);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
setDiskFreeSpace(result);
});
};
const handleChooseDownloadsPath = async () => { const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({ const { filePaths } = await window.electron.showOpenDialog({
defaultPath: selectedPath, defaultPath: selectedPath,
@ -124,7 +141,7 @@ export function DownloadSettingsModal({
visible={visible} visible={visible}
title={t("download_settings")} title={t("download_settings")}
description={t("space_left_on_disk", { description={t("space_left_on_disk", {
space: formatBytes(diskFreeSpace?.free ?? 0), space: formatBytes(diskFreeSpace ?? 0),
})} })}
onClose={onClose} onClose={onClose}
> >
@ -168,23 +185,32 @@ export function DownloadSettingsModal({
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
}} }}
> >
<div className={styles.downloadsPathField}> <TextField
<TextField value={selectedPath}
value={selectedPath} readOnly
readOnly disabled
disabled label={t("download_path")}
label={t("download_path")} error={
/> hasWritePermission === false ? (
<span
<Button className={styles.pathError}
style={{ alignSelf: "flex-end" }} data-open-article="cannot-write-directory"
theme="outline" >
onClick={handleChooseDownloadsPath} {t("no_write_permission")}
disabled={downloadStarting} </span>
> ) : undefined
{t("change")} }
</Button> rightContent={
</div> <Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
disabled={downloadStarting}
>
{t("change")}
</Button>
}
/>
<p className={styles.hintText}> <p className={styles.hintText}>
<Trans i18nKey="select_folder_hint" ns="game_details"> <Trans i18nKey="select_folder_hint" ns="game_details">
@ -195,7 +221,11 @@ export function DownloadSettingsModal({
<Button <Button
onClick={handleStartClick} onClick={handleStartClick}
disabled={downloadStarting || selectedDownloader === null} disabled={
downloadStarting ||
selectedDownloader === null ||
!hasWritePermission
}
> >
<DownloadIcon /> <DownloadIcon />
{t("download_now")} {t("download_now")}

View File

@ -163,7 +163,7 @@ export function EditProfileModal(
minLength={3} minLength={3}
maxLength={50} maxLength={50}
containerProps={{ style: { width: "100%" } }} containerProps={{ style: { width: "100%" } }}
error={errors.displayName} error={errors.displayName?.message}
/> />
</div> </div>

View File

@ -105,7 +105,7 @@ export function ReportProfile() {
{...register("description")} {...register("description")}
label={t("report_description")} label={t("report_description")}
placeholder={t("report_description_placeholder")} placeholder={t("report_description_placeholder")}
error={errors.description} error={errors.description?.message}
/> />
<Button <Button

View File

@ -150,7 +150,7 @@ export function AddDownloadSourceModal({
{...register("url")} {...register("url")}
label={t("download_source_url")} label={t("download_source_url")}
placeholder={t("insert_valid_json_url")} placeholder={t("insert_valid_json_url")}
error={errors.url} error={errors.url?.message}
rightContent={ rightContent={
<Button <Button
type="button" type="button"

View File

@ -4381,11 +4381,6 @@ chalk@^5.3.0:
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
check-disk-space@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.4.0.tgz#eb8e69eee7a378fd12e35281b8123a8b4c4a8ff7"
integrity sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==
chokidar@^3.5.3: chokidar@^3.5.3:
version "3.6.0" version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
@ -4921,6 +4916,14 @@ dir-glob@^3.0.1:
dependencies: dependencies:
path-type "^4.0.0" path-type "^4.0.0"
diskusage@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/diskusage/-/diskusage-1.2.0.tgz#3e8ae42333d5d7e0c7d93e055d7fea9ea841bc88"
integrity sha512-2u3OG3xuf5MFyzc4MctNRUKjjwK+UkovRYdD2ed/NZNZPrt0lqHnLKxGhlFVvAb4/oufIgQG3nWgwmeTbHOvXA==
dependencies:
es6-promise "^4.2.8"
nan "^2.18.0"
dmg-builder@25.1.8: dmg-builder@25.1.8:
version "25.1.8" version "25.1.8"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.8.tgz#41f3b725edd896156e891016a44129e1bd580430" resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.8.tgz#41f3b725edd896156e891016a44129e1bd580430"
@ -5327,6 +5330,11 @@ es6-error@^4.1.1:
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
esbuild@^0.21.3, esbuild@^0.21.5: esbuild@^0.21.3, esbuild@^0.21.5:
version "0.21.5" version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
@ -7425,6 +7433,11 @@ mz@^2.4.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" thenify-all "^1.0.0"
nan@^2.18.0:
version "2.22.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3"
integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==
nanoid@^3.3.7: nanoid@^3.3.7:
version "3.3.7" version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"