diff --git a/.eslintignore b/.eslintignore index d3510226..1f026f84 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,5 @@ artwork/* build/* -electron/* gulp/* node_modules/* res/* diff --git a/electron/index.js b/electron/index.js index 264aa581..216833af 100644 --- a/electron/index.js +++ b/electron/index.js @@ -1,48 +1,55 @@ -/* eslint-disable quotes,no-undef */ - -const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell, dialog, session } = require("electron"); +const { app, BrowserWindow, Menu, ipcMain, shell, session } = require("electron"); +const { initializeMenu } = require("./src/menu"); +const { initializeSwitches, isLocal, isDev, shouldHideDevtools } = require("./src/switches"); +const { initializeFolders } = require("./src/folders"); +const { initializeCrashLogs } = require("./src/crashlogs"); +const { initializeFilesystem } = require("./src/filesystem"); +const { showMissingExternalMods, initializeMods, showModErrors } = require("./src/mods"); +const { initializeSteam } = require("./src/steam"); const path = require("path"); -const url = require("url"); -const fs = require("fs"); -const steam = require("./steam"); -const asyncLock = require("async-lock"); const windowStateKeeper = require("electron-window-state"); -// Disable hardware key handling, i.e. being able to pause/resume the game music -// with hardware keys -app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling"); - -const isDev = app.commandLine.hasSwitch("dev"); -const isLocal = app.commandLine.hasSwitch("local"); -const safeMode = app.commandLine.hasSwitch("safe-mode"); -const externalMod = app.commandLine.getSwitchValue("load-mod"); - -const roamingFolder = - process.env.APPDATA || - (process.platform == "darwin" - ? process.env.HOME + "/Library/Preferences" - : process.env.HOME + "/.local/share"); - -let storePath = path.join(roamingFolder, "shapez.io", "saves"); -let modsPath = path.join(roamingFolder, "shapez.io", "mods"); - -if (!fs.existsSync(storePath)) { - // No try-catch by design - fs.mkdirSync(storePath, { recursive: true }); -} - -if (!fs.existsSync(modsPath)) { - fs.mkdirSync(modsPath, { recursive: true }); -} - /** @type {BrowserWindow} */ let win = null; -let menu = null; -function createWindow() { - let faviconExtension = ".png"; +async function onReady() { + // Show external mod errors before we open anything + await showMissingExternalMods(); + + // Create a new in-memory session + const appSession = session.fromPartition("default"); + win = createWindow(appSession); + + win.once("ready-to-show", () => { + win.show(); + + if (isDev && !shouldHideDevtools) { + // Show developer tools initially + win.webContents.toggleDevTools(); + } + }); + win.on("closed", () => (win = null)); + + ipcMain.on("set-fullscreen", (_, flag) => win.setFullScreen(flag)); +} + +/** + * Opens a URL in external browser if it's HTTPS, does + * nothing if it isn't. + * @param {string} url + */ +function openSecureURL(url) { + if (!url.startsWith("https://")) { + return; + } + + return shell.openExternal(url); +} + +function createWindow(appSession) { + let faviconName = "favicon.png"; if (process.platform === "win32") { - faviconExtension = ".ico"; + faviconName = "favicon.ico"; } const mainWindowState = windowStateKeeper({ @@ -50,340 +57,93 @@ function createWindow() { defaultHeight: 800, }); - win = new BrowserWindow({ + const window = new BrowserWindow({ x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, height: mainWindowState.height, show: false, backgroundColor: "#222428", - useContentSize: false, minWidth: 800, minHeight: 600, title: "shapez.io Standalone", - transparent: false, - icon: path.join(__dirname, "favicon" + faviconExtension), - // fullscreen: true, + icon: path.join(__dirname, faviconName), autoHideMenuBar: !isDev, webPreferences: { - nodeIntegration: false, - nodeIntegrationInWorker: false, - nodeIntegrationInSubFrames: false, - contextIsolation: true, - enableRemoteModule: false, disableBlinkFeatures: "Auxclick", - - webSecurity: true, sandbox: true, - preload: path.join(__dirname, "preload.js"), - experimentalFeatures: false, + preload: path.join(__dirname, "src/preload.js"), + session: appSession, }, - allowRunningInsecureContent: false, }); - mainWindowState.manage(win); + mainWindowState.manage(window); if (isLocal) { - win.loadURL("http://localhost:3005"); + window.loadURL("http://localhost:3005"); } else { - win.loadURL( - url.format({ - pathname: path.join(__dirname, "index.html"), - protocol: "file:", - slashes: true, - }) - ); + window.loadFile("index.html"); } - win.webContents.session.clearCache(); - win.webContents.session.clearStorageData(); - - ////// SECURITY // Disable permission requests - win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { - callback(false); - }); - session.fromPartition("default").setPermissionRequestHandler((webContents, permission, callback) => { + window.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => { callback(false); }); - app.on("web-contents-created", (event, contents) => { - // Disable vewbiew - contents.on("will-attach-webview", (event, webPreferences, params) => { - event.preventDefault(); - }); - // Disable navigation - contents.on("will-navigate", (event, navigationUrl) => { - event.preventDefault(); - }); - }); - - win.webContents.on("will-redirect", (contentsEvent, navigationUrl) => { - // Log and prevent the app from redirecting to a new page - console.error( - `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.` - ); - contentsEvent.preventDefault(); - }); - - // Filter loading any module via remote; - // you shouldn't be using remote at all, though - // https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module - app.on("remote-require", (event, webContents, moduleName) => { - event.preventDefault(); - }); - - // built-ins are modules such as "app" - app.on("remote-get-builtin", (event, webContents, moduleName) => { - event.preventDefault(); - }); - - app.on("remote-get-global", (event, webContents, globalName) => { - event.preventDefault(); - }); - - app.on("remote-get-current-window", (event, webContents) => { - event.preventDefault(); - }); - - app.on("remote-get-current-web-contents", (event, webContents) => { - event.preventDefault(); - }); - - //// END SECURITY - - win.webContents.on("new-window", (event, pth) => { - event.preventDefault(); - - if (pth.startsWith("https://")) { - shell.openExternal(pth); + window.webContents.on("will-navigate", (event, url) => { + if (url == window.webContents.getURL()) { + // Allow location.reload() + return; } + + event.preventDefault(); + openSecureURL(url); }); - win.on("closed", () => { - console.log("Window closed"); - win = null; + window.webContents.on("will-redirect", event => { + event.preventDefault(); + }); + + window.webContents.setWindowOpenHandler(({ url }) => { + openSecureURL(url); + return { action: "deny" }; }); if (isDev) { - menu = new Menu(); - - win.webContents.toggleDevTools(); - - const mainItem = new MenuItem({ - label: "Toggle Dev Tools", - click: () => win.webContents.toggleDevTools(), - accelerator: "F12", - }); - menu.append(mainItem); - - const reloadItem = new MenuItem({ - label: "Reload", - click: () => win.reload(), - accelerator: "F5", - }); - menu.append(reloadItem); - - const fullscreenItem = new MenuItem({ - label: "Fullscreen", - click: () => win.setFullScreen(!win.isFullScreen()), - accelerator: "F11", - }); - menu.append(fullscreenItem); - - const mainMenu = new Menu(); - mainMenu.append( - new MenuItem({ - label: "shapez.io", - submenu: menu, - }) - ); - - Menu.setApplicationMenu(mainMenu); + initializeMenu(window); } else { Menu.setApplicationMenu(null); } - win.once("ready-to-show", () => { - win.show(); - win.focus(); - }); + return window; } +initializeSwitches(); +initializeFolders(); +initializeCrashLogs(); + if (!app.requestSingleInstanceLock()) { + // Already running + app.exit(); +} + +app.on("ready", onReady); +app.on("second-instance", () => { + // Someone tried to run a second instance, we should focus + if (win) { + if (win.isMinimized()) { + win.restore(); + } + win.focus(); + } +}); + +ipcMain.on("restart-app", () => { + app.relaunch(); app.exit(0); -} else { - app.on("second-instance", () => { - // Someone tried to run a second instance, we should focus - if (win) { - if (win.isMinimized()) { - win.restore(); - } - win.focus(); - } - }); -} - -app.on("ready", createWindow); - -app.on("window-all-closed", () => { - console.log("All windows closed"); - app.quit(); }); +ipcMain.on("exit-app", () => app.quit()); -ipcMain.on("set-fullscreen", (event, flag) => { - win.setFullScreen(flag); -}); - -ipcMain.on("exit-app", () => { - win.close(); - app.quit(); -}); - -let renameCounter = 1; - -const fileLock = new asyncLock({ - timeout: 30000, - maxPending: 1000, -}); - -function niceFileName(filename) { - return filename.replace(storePath, "@"); -} - -async function writeFileSafe(filename, contents) { - ++renameCounter; - const prefix = "[ " + renameCounter + ":" + niceFileName(filename) + " ] "; - const transactionId = String(new Date().getTime()) + "." + renameCounter; - - if (fileLock.isBusy()) { - console.warn(prefix, "Concurrent write process on", filename); - } - - fileLock.acquire(filename, async () => { - console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId); - - if (!fs.existsSync(filename)) { - // this one is easy - console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename)); - await fs.promises.writeFile(filename, contents, "utf8"); - return; - } - - // first, write a temporary file (.tmp-XXX) - const tempName = filename + ".tmp-" + transactionId; - console.log(prefix, "Writing temporary file", niceFileName(tempName)); - await fs.promises.writeFile(tempName, contents, "utf8"); - - // now, rename the original file to (.backup-XXX) - const oldTemporaryName = filename + ".backup-" + transactionId; - console.log( - prefix, - "Renaming old file", - niceFileName(filename), - "to", - niceFileName(oldTemporaryName) - ); - await fs.promises.rename(filename, oldTemporaryName); - - // now, rename the temporary file (.tmp-XXX) to the target - console.log( - prefix, - "Renaming the temporary file", - niceFileName(tempName), - "to the original", - niceFileName(filename) - ); - await fs.promises.rename(tempName, filename); - - // we are done now, try to create a backup, but don't fail if the backup fails - try { - // check if there is an old backup file - const backupFileName = filename + ".backup"; - if (fs.existsSync(backupFileName)) { - console.log(prefix, "Deleting old backup file", niceFileName(backupFileName)); - // delete the old backup - await fs.promises.unlink(backupFileName); - } - - // rename the old file to the new backup file - console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location"); - await fs.promises.rename(oldTemporaryName, backupFileName); - } catch (ex) { - console.error(prefix, "Failed to switch backup files:", ex); - } - }); -} - -ipcMain.handle("fs-job", async (event, job) => { - const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/gi, "_"); - const fname = path.join(storePath, filenameSafe); - switch (job.type) { - case "read": { - if (!fs.existsSync(fname)) { - // Special FILE_NOT_FOUND error code - return { error: "file_not_found" }; - } - return await fs.promises.readFile(fname, "utf8"); - } - case "write": { - await writeFileSafe(fname, job.contents); - return job.contents; - } - - case "delete": { - await fs.promises.unlink(fname); - return; - } - - default: - throw new Error("Unknown fs job: " + job.type); - } -}); - -ipcMain.handle("open-mods-folder", async () => { - shell.openPath(modsPath); -}); - -console.log("Loading mods ..."); - -function loadMods() { - if (safeMode) { - console.log("Safe Mode enabled for mods, skipping mod search"); - } - console.log("Loading mods from", modsPath); - let modFiles = safeMode - ? [] - : fs - .readdirSync(modsPath) - .filter(filename => filename.endsWith(".js")) - .map(filename => path.join(modsPath, filename)); - - if (externalMod) { - console.log("Adding external mod source:", externalMod); - const externalModPaths = externalMod.split(","); - modFiles = modFiles.concat(externalModPaths); - } - - return modFiles.map(filename => fs.readFileSync(filename, "utf8")); -} - -let mods = []; -try { - mods = loadMods(); - console.log("Loaded", mods.length, "mods"); -} catch (ex) { - console.error("Failed to load mods"); - dialog.showErrorBox("Failed to load mods:", ex); -} - -ipcMain.handle("get-mods", async () => { - return mods; -}); - -steam.init(isDev); - -// Only allow achievements and puzzle DLC if no mods are loaded -if (mods.length === 0) { - steam.listen(); -} +initializeFilesystem(); +initializeMods(); +initializeSteam(); diff --git a/electron/jsconfig.json b/electron/jsconfig.json new file mode 100644 index 00000000..37066796 --- /dev/null +++ b/electron/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "CommonJS" + }, + "include": ["./src/**/*.js", "index.js"] +} diff --git a/electron/mods/README.txt b/electron/mods/README.txt deleted file mode 100644 index 666cc18f..00000000 --- a/electron/mods/README.txt +++ /dev/null @@ -1,6 +0,0 @@ -Here you can place mods. Every mod should be a single file ending with ".js". - ---- WARNING --- -Mods can potentially access to your filesystem. -Please only install mods from trusted sources and developers. ---- WARNING --- diff --git a/electron/package.json b/electron/package.json index a0b97f6f..bdb20c52 100644 --- a/electron/package.json +++ b/electron/package.json @@ -9,13 +9,14 @@ "startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local", "start": "electron --disable-direct-composition --in-process-gpu ." }, - "devDependencies": {}, + "devDependencies": { + "electron": "^18.3.1" + }, "optionalDependencies": { "shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v99" }, "dependencies": { "async-lock": "^1.2.8", - "electron": "16.0.7", "electron-window-state": "^5.0.3" } } diff --git a/electron/src/crashlogs.js b/electron/src/crashlogs.js new file mode 100644 index 00000000..2ab075e1 --- /dev/null +++ b/electron/src/crashlogs.js @@ -0,0 +1,57 @@ +const { app, ipcMain, shell } = require("electron"); +const { writeFile } = require("fs/promises"); +const path = require("path"); +const { crashLogsDir } = require("./folders"); + +/** + * Writes a crash log and reveals it in file manager. + * @param {string} errorStack Stacktrace from renderer + */ +async function writeCrashLog(errorStack) { + const separator = `\n${"=".repeat(20)}\n\n`; + let contents = errorStack + separator; + + contents += "GPU Features:\n"; + + const gpuFeatures = Object.entries(app.getGPUFeatureStatus()); + for (const [feature, status] of gpuFeatures) { + contents += `${feature}:\t${status}`; + } + + const gpuInfo = await app.getGPUInfo("basic"); + contents += separator; + + for (const gpu of gpuInfo.gpuDevice) { + contents += "GPU Active: " + (gpu.active ? "Yes" : "No"); + contents += "Vendor: 0x" + gpu.vendorId.toString(16); + contents += "Device: 0x" + gpu.deviceId.toString(16); + contents += "\n"; + } + + const date = new Date().toISOString(); + contents += separator + "Date: " + date; + + const target = path.join(crashLogsDir, date + ".log"); + await writeFile(target, contents, "utf-8"); + console.log("Wrote crash log to", path.basename(target)); + + shell.showItemInFolder(target); +} + +/** + * Setup an IPC handler to write and reveal crash logs as soon + * as an error occurs. + */ +function initializeCrashLogs() { + ipcMain.on("write-crash-log", (_, stack) => { + // Write crash logs if we've been told to + writeCrashLog(stack); + }); + + // Also attempt to catch main process errors + process.on("uncaughtException", error => { + writeCrashLog(error.stack); + }); +} + +module.exports = { initializeCrashLogs }; diff --git a/electron/src/filesystem.js b/electron/src/filesystem.js new file mode 100644 index 00000000..a45fd8cb --- /dev/null +++ b/electron/src/filesystem.js @@ -0,0 +1,107 @@ +const AsyncLock = require("async-lock"); +const { ipcMain, shell } = require("electron"); +const { existsSync } = require("fs"); +const { unlink, readFile, writeFile, rename } = require("fs/promises"); +const path = require("path"); +const { savesDir } = require("./folders"); + +let renameCounter = 1; + +const fileLock = new AsyncLock({ + timeout: 30000, + maxPending: 1000, +}); + +/** + * Generic handler for FS jobs. + * @param {{ type: "read"|"write"|"delete"|"reveal", filename: string, contents?: any }} job + */ +async function onFilesystemJob(_, job) { + const safeFileName = sanitizeFileName(job.filename); + const filePath = path.join(savesDir, safeFileName); + + switch (job.type) { + case "read": + if (!existsSync(filePath)) { + // Notify the renderer + return { error: "file_not_found" }; + } + + return await readFile(filePath, "utf-8"); + case "write": + await writeFileSafe(filePath, job.contents); + return job.contents; + case "delete": + await unlink(filePath); + return; + case "reveal": + shell.showItemInFolder(filePath); + return; + default: + throw new Error("Unknown FS job: " + job.type); + } +} + +async function writeFileSafe(file, contents) { + renameCounter++; + const prefix = `[ ${renameCounter}:${path.basename(file)} ] `; + const transactionId = Date.now() + "." + renameCounter; + + if (fileLock.isBusy()) { + console.warn(prefix, "Concurrent write process on", file); + } + + fileLock.acquire(file, async () => { + console.log(prefix, "Starting write in transaction", transactionId); + + if (!existsSync(file)) { + // This one is easy - write directly + console.log(prefix, "Creating a new file"); + await writeFile(file, contents, "utf-8"); + return; + } + + // First, write a temporary file (.tmp-XXX) + const tempName = file + ".tmp-" + transactionId; + console.log(prefix, "Writing temporary file", path.basename(tempName)); + await writeFile(tempName, contents, "utf-8"); + + // Now, rename the original file to (.backup-XXX) + const oldTemporaryName = file + ".backup-" + transactionId; + console.log(prefix, "Renaming old file to", path.basename(oldTemporaryName)); + await rename(file, oldTemporaryName); + + // Now, rename the temporary file (.tmp-XXX) to the target + console.log(prefix, "Renaming the temporary file", path.basename(tempName), "to the original file"); + await rename(tempName, file); + + // We are done now, try to create a backup, but don't fail if the backup fails + try { + // Check if there is an old backup file + const backupFileName = file + ".backup"; + if (existsSync(backupFileName)) { + console.log(prefix, "Deleting old backup file", path.basename(backupFileName)); + await unlink(backupFileName); + } + + // Rename the old file to the new backup file + console.log(prefix, "Moving", path.basename(oldTemporaryName), "to the backup file location"); + await rename(oldTemporaryName, backupFileName); + } catch (err) { + console.error(prefix, "Failed to swap backup files:", err); + } + }); +} + +function sanitizeFileName(filename) { + return filename.replace(/[^a-z.\-_0-9]/gi, "_"); +} + +/** + * Registers IPC handler for filesystem-related tasks. + */ +function initializeFilesystem() { + ipcMain.handle("fs-job", onFilesystemJob); +} + +module.exports = { initializeFilesystem }; diff --git a/electron/src/folders.js b/electron/src/folders.js new file mode 100644 index 00000000..daf34762 --- /dev/null +++ b/electron/src/folders.js @@ -0,0 +1,51 @@ +const { app, ipcMain, shell } = require("electron"); +const { mkdirSync } = require("fs"); +const path = require("path"); + +// Need this to avoid migrating savegames and mods +const userHome = app.getPath("home"); +const platformStorageRoots = { + win32: app.getPath("appData"), + linux: process.env.XDG_DATA_HOME ?? path.join(userHome, ".local/share"), + darwin: path.join(userHome, "Library/Preferences"), +}; + +app.setPath("appData", platformStorageRoots[process.platform]); + +const appData = path.join(app.getPath("appData"), "shapez.io"); +const savesDir = path.join(appData, "saves"); +const modsDir = path.join(appData, "mods"); +const crashLogsDir = path.join(appData, "crashes"); + +// Here, { recursive: true } permits omitting existsSync check +mkdirSync(savesDir, { recursive: true }); +mkdirSync(modsDir, { recursive: true }); +mkdirSync(crashLogsDir, { recursive: true }); + +// Folders need to exist before it is possible to set them +app.setPath("userData", appData); + +/** + * Sets IPC handler to open various folders. + */ +function initializeFolders() { + ipcMain.handle("open-folder", (_, folder) => { + const folderPath = { + saves: savesDir, + mods: modsDir, + }[folder]; + + if (folderPath === undefined) { + // Asked to open unknown folder + return; + } + return shell.openPath(folderPath); + }); +} + +module.exports = { + initializeFolders, + savesDir, + modsDir, + crashLogsDir, +}; diff --git a/electron/src/menu.js b/electron/src/menu.js new file mode 100644 index 00000000..8960ebb4 --- /dev/null +++ b/electron/src/menu.js @@ -0,0 +1,71 @@ +const { Menu, MenuItem, app } = require("electron"); + +/** + * Returns menu items for the specified window. + * @param {Electron.BrowserWindow} window The window to use for actions + */ +function createMenuItems(window) { + /** + * Specifying options directly for simplicity. + * @type {Electron.MenuItemConstructorOptions[]} + */ + const itemOptions = []; + + itemOptions.push({ + label: "Developer Tools", + accelerator: "F12", + click: () => window.webContents.toggleDevTools(), + }); + + itemOptions.push({ + label: "Reload", + accelerator: "F5", + click: () => window.reload(), + }); + + itemOptions.push({ + label: "Restart", + accelerator: "F5", + click: () => { + app.relaunch(); + app.exit(0); + }, + }); + + itemOptions.push({ + label: "Full Screen", + accelerator: "F11", + click: () => window.setFullScreen(!window.fullScreen), + }); + + return itemOptions.map(options => new MenuItem(options)); +} + +/** + * Create and set a menu for quick access to development tasks. + * @param {Electron.BrowserWindow} window The window to set menu on + */ +function initializeMenu(window) { + const menu = new Menu(); + for (const item of createMenuItems(window)) { + menu.append(item); + } + + if (process.platform == "darwin") { + // We're on macOS, so a root menu is needed + const rootMenu = new Menu(); + rootMenu.append( + new MenuItem({ + label: "shapez.io", + submenu: menu, + }) + ); + + window.setMenu(rootMenu); + } + + // Items can be directly used on Windows/Linux + Menu.setApplicationMenu(menu); +} + +module.exports = { initializeMenu }; diff --git a/electron/src/mods.js b/electron/src/mods.js new file mode 100644 index 00000000..6f1b00bb --- /dev/null +++ b/electron/src/mods.js @@ -0,0 +1,136 @@ +const { dialog, ipcMain } = require("electron"); +const { readdirSync, readFileSync } = require("fs"); +const { basename, join } = require("path"); +const { modsDir } = require("./folders"); +const { externalModList, isSafeMode, isDev } = require("./switches"); + +/** + * Map of mod files to source code, populated when initializing mods. + * @type {Map} + * @todo Leverage this to implement disalbing mods + */ +const modSources = new Map(); + +/** + * Stores all mod loading errors to report them later. + * @type {Map} + */ +const modErrors = new Map(); + +/** + * Returns an array of all mod files found in mods/ directory, + * skipping the search if safe mode is turned on. + * @return {string[]} + */ +function getModFiles() { + if (isSafeMode) { + return []; + } + + const files = readdirSync(modsDir).filter(file => file.endsWith(".js")); + return files.map(file => join(modsDir, file)); +} + +/** + * Tries to read all mod files and store their source code in a map, + * and registers an IPC handler to return mod source code. + */ +function initializeMods() { + // Not checking whether external mods exist, it's done later + const loadOrder = [...getModFiles(), ...externalModList]; + + for (const file of loadOrder) { + // Each mod has own try/catch block so a single mod won't + // break everything else + try { + const code = readFileSync(file, "utf-8"); + modSources.set(file, code); + } catch (err) { + if (err instanceof Error) { + // Ensure only Error objects get there + modErrors.set(basename(file), err); + } else { + // Otherwise, silently log them - the mod is throwing + // random stuff + console.error("A mod reported unknown error:", err); + } + + console.warn("Failed to load a mod:", file); + } + } + + console.log(modSources.size, "mod files found"); + + ipcMain.handle("get-mods", () => { + // Renderer only needs relative file names + /** @type {{ filename: string, source: string }[]} */ + const mods = []; + for (const [file, source] of modSources.entries()) { + // Note: duplicates are possible, so we're not using a map here + mods.push({ filename: basename(file), source }); + } + + return mods; + }); + + ipcMain.on("mod-error", (_, filename, error) => { + // A single mod can't have more than one error, + // so just set filename -> error + modErrors.set(filename, error); + }); + + ipcMain.on("show-mod-errors", () => showModErrors()); +} + +/** + * Displays a warning about missing external mod files. + * Make sure to call this after "ready" app event. + */ +function showMissingExternalMods() { + const missing = externalModList.filter(mod => !existsSync(mod)); + if (missing.length == 0) { + // None missing, or none were specified + return; + } + + const message = missing.map(mod => ` - ${mod}`).join("\n"); + return dialog.showMessageBox({ + title: "External Mod Errors", + message: "These mod files could not be found:", + detail: message, + type: "warning", + }); +} + +/** + * Shows a dialog with collected mod loading errors, if + * there were any. Can only be called once the app is ready. + */ +function showModErrors() { + if (modErrors.size == 0) { + // We're lucky - no errors reported + return; + } + + let errorText = ""; + for (const [mod, error] of modErrors.entries()) { + // Show full errors with --dev + errorText += `${mod}: ${isDev ? "\n" + error.stack : error.message}\n`; + } + + return dialog.showMessageBox({ + title: "Mod Errors", + message: "Failed to load some mods:", + detail: errorText, + }); +} + +function anyModLoaded() { + return modSources.size > 0; +} + +module.exports = { + initializeMods, + showMissingExternalMods, + anyModLoaded, +}; diff --git a/electron/preload.js b/electron/src/preload.js similarity index 100% rename from electron/preload.js rename to electron/src/preload.js diff --git a/electron/src/steam.js b/electron/src/steam.js new file mode 100644 index 00000000..c7b48f69 --- /dev/null +++ b/electron/src/steam.js @@ -0,0 +1,113 @@ +const { readFileSync } = require("fs"); +const { join } = require("path"); +const { ipcMain, app, dialog } = require("electron"); +const { isDev } = require("./switches"); +const { anyModLoaded } = require("./mods"); + +let greenworks = null; +let appId = null; +let isInitialized = false; + +try { + greenworks = require("shapez.io-private-artifacts/steam/greenworks"); + appId = parseInt(readFileSync("steam_appid.txt", "utf-8")); +} catch (err) { + console.warn("Failed to load Steam API:", err.message); +} + +/** + * Restarts the game immediately if Steam cannot be initialized + * without restarting (game launched outside of Steam). Doesn't + * restart if shapez.io was launched with --dev. + */ +function restartIfNeeded() { + if (isDev) { + // Skip restart when in development mode + return; + } + + if (greenworks.restartAppIfNecessary(appId)) { + console.log("Restarting with Steam..."); + app.exit(0); + } +} + +/** + * Initializes Steam API (if possible) and registers relevant + * IPC handlers for achievements and DLC. Can be safely run + * before "ready" event. + */ +function initializeSteam() { + ipcMain.handle("steam:is-initialized", () => isInitialized); + ipcMain.handle("steam:activate-achievement", (_, id) => activateAchievement(id)); + ipcMain.handle("steam:check-app-ownership", (_, id) => checkAppOwnership(id)); + ipcMain.handle("steam:get-ticket", () => getTicket()); + + if (greenworks === null || anyModLoaded()) { + // Skip initialization - we won't need it anyway + return; + } + + restartIfNeeded(); + + try { + isInitialized = greenworks.init(); + } catch (err) { + // This mostly happens when Steam glitches occur + dialog.showErrorBox("Steam API Error", err.message); + app.exit(1); + } +} + +/** + * Activates an achievement by ID, if the game isn't running + * with mods. + * @param {string} id ID of achievement to activate + */ +function activateAchievement(id) { + if (!isInitialized) { + // Either missing greenworks or running with mods + return; + } + + return new Promise((resolve, reject) => { + greenworks.activateAchievement( + id, + () => resolve(), + err => reject(err) + ); + }); +} + +/** + * Checks whether the user owns specified app, used for DLC. + * @param {number} id ID of application to check ownership of + */ +function checkAppOwnership(id) { + if (!isInitialized) { + // No Steam access, therefore it's impossible to check + return false; + } + + return greenworks.isDLCInstalled(id); +} + +function getTicket() { + if (!isInitialized) { + // Just fail because there's nothing to do + return Promise.reject(new Error("Steam API is not initialized.")); + } + + console.log("Requesting Steam ticket..."); + return new Promise((resolve, reject) => { + greenworks.getAuthSessionTicket( + success => resolve(success.ticket.toString("hex")), + error => { + console.error("Failed to get steam ticket:", error); + reject(error); + } + ); + }); +} + +module.exports = { initializeSteam }; diff --git a/electron/src/switches.js b/electron/src/switches.js new file mode 100644 index 00000000..ef46120f --- /dev/null +++ b/electron/src/switches.js @@ -0,0 +1,85 @@ +const { app, dialog } = require("electron"); +const { readFileSync } = require("fs"); +const path = require("path"); + +/** + * List of mods to load manually + * @type {string[]} + */ +const externalModList = []; + +/** + * Retrieves external mod list and sets up the application, + * should be called only before the app is ready. + */ +function initializeSwitches() { + if (!app.commandLine.hasSwitch("disable-features")) { + // Disable Chromium's media keys handler to avoid interfering with + // media controls and other apps (such as pausing in-game music) + app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling"); + } + + // First load mod list, then other mods + externalModList.push(...parseExternalModList()); + externalModList.push(...parseCommandLineMods()); +} + +/** + * Returns an array of absolute file paths to mods loaded using + * --load-mod=path/mod1.js,path/mod2.js argument, or none if the + * argument is missing. + */ +function parseCommandLineMods() { + const loadModList = app.commandLine.getSwitchValue("load-mod"); + if (!loadModList) { + // Empty or missing + return []; + } + + const files = loadModList.split(","); + return resolveAllFiles(files); +} + +/** + * Returns an array of absolute file paths to mods loaded using + * --mod-list=path/to/mods.json argument (JSON array), or none + * if the file wasn't specified or found. + */ +function parseExternalModList() { + const modListPath = app.commandLine.getSwitchValue("mod-list"); + if (!modListPath) { + // None requested, let's just skip that + return []; + } + + try { + // Read the file and return resolved mod file paths + const json = readFileSync(modListPath, "utf-8"); + return resolveAllFiles(JSON.parse(json)); + } catch (err) { + // Something went wrong - notify and continue + dialog.showErrorBox("Failed to load external mod list!", err.stack); + return []; + } +} + +/** + * Small utility to resolve all file paths in an array. + * @param {string[]} files + */ +function resolveAllFiles(files) { + return files.map(file => path.resolve(file)); +} + +module.exports = { + initializeSwitches, + // Shows a menu on the window with useful actions + isDev: app.commandLine.hasSwitch("dev"), + // Instructs the renderer to use bundle hosted on localhost + isLocal: app.commandLine.hasSwitch("local"), + // Disables all mods except manually loaded ones + isSafeMode: app.commandLine.hasSwitch("safe-mode"), + // Suppresses initial toggle of developer tools + shouldHideDevtools: app.commandLine.hasSwitch("hide-devtools"), + externalModList, +}; diff --git a/electron/steam.js b/electron/steam.js deleted file mode 100644 index cdda540b..00000000 --- a/electron/steam.js +++ /dev/null @@ -1,112 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const { ipcMain } = require("electron"); - -let greenworks = null; -let appId = null; -let initialized = false; - -try { - greenworks = require("shapez.io-private-artifacts/steam/greenworks"); - appId = parseInt(fs.readFileSync(path.join(__dirname, "steam_appid.txt"), "utf8")); -} catch (err) { - // greenworks is not installed - console.warn("Failed to load steam api:", err); -} - -console.log("App ID:", appId); - -function init(isDev) { - if (!greenworks) { - return; - } - - if (!isDev) { - if (greenworks.restartAppIfNecessary(appId)) { - console.log("Restarting ..."); - process.exit(0); - } - } - - if (!greenworks.init()) { - console.log("Failed to initialize greenworks"); - process.exit(1); - } - - initialized = true; -} - -function listen() { - ipcMain.handle("steam:is-initialized", isInitialized); - - if (!initialized) { - console.warn("Steam not initialized, won't be able to listen"); - return; - } - - if (!greenworks) { - console.warn("Greenworks not loaded, won't be able to listen"); - return; - } - - console.log("Adding listeners"); - - ipcMain.handle("steam:get-achievement-names", getAchievementNames); - ipcMain.handle("steam:activate-achievement", activateAchievement); - - function bufferToHex(buffer) { - return Array.from(new Uint8Array(buffer)) - .map(b => b.toString(16).padStart(2, "0")) - .join(""); - } - - ipcMain.handle("steam:get-ticket", (event, arg) => { - console.log("Requested steam ticket ..."); - return new Promise((resolve, reject) => { - greenworks.getAuthSessionTicket( - success => { - const ticketHex = bufferToHex(success.ticket); - resolve(ticketHex); - }, - error => { - console.error("Failed to get steam ticket:", error); - reject(error); - } - ); - }); - }); - - ipcMain.handle("steam:check-app-ownership", (event, appId) => { - return Promise.resolve(greenworks.isDLCInstalled(appId)); - }); -} - -function isInitialized(event) { - return Promise.resolve(initialized); -} - -function getAchievementNames(event) { - return new Promise((resolve, reject) => { - try { - const achievements = greenworks.getAchievementNames(); - resolve(achievements); - } catch (err) { - reject(err); - } - }); -} - -function activateAchievement(event, id) { - return new Promise((resolve, reject) => { - greenworks.activateAchievement( - id, - () => resolve(), - err => reject(err) - ); - }); -} - -module.exports = { - init, - listen, -}; diff --git a/electron/yarn.lock b/electron/yarn.lock index 626235de..5a685b60 100644 --- a/electron/yarn.lock +++ b/electron/yarn.lock @@ -3,9 +3,9 @@ "@electron/get@^1.13.0": - version "1.13.1" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.13.1.tgz#42a0aa62fd1189638bd966e23effaebb16108368" - integrity sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA== + version "1.14.1" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.14.1.tgz#16ba75f02dffb74c23965e72d617adc721d27f40" + integrity sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw== dependencies: debug "^4.1.1" env-paths "^2.2.0" @@ -30,10 +30,10 @@ dependencies: defer-to-connect "^1.0.1" -"@types/node@^14.6.2": - version "14.18.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.5.tgz#0dd636fe7b2c6055cbed0d4ca3b7fb540f130a96" - integrity sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A== +"@types/node@^16.11.26": + version "16.11.36" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.36.tgz#9ab9f8276987132ed2b225cace2218ba794fc751" + integrity sha512-FR5QJe+TaoZ2GsMHkjuwoNabr+UrJNRr2HNOo+r/7vhcuntM6Ee/pRPOnRhhL2XE9OOvX9VLEq+BcXl3VjNoWA== async-lock@^1.2.8: version "1.2.8" @@ -149,13 +149,13 @@ electron-window-state@^5.0.3: jsonfile "^4.0.0" mkdirp "^0.5.1" -electron@16.0.7: - version "16.0.7" - resolved "https://registry.yarnpkg.com/electron/-/electron-16.0.7.tgz#87eaccd05ab61563d3c17dfbad2949bba7ead162" - integrity sha512-/IMwpBf2svhA1X/7Q58RV+Nn0fvUJsHniG4TizaO7q4iKFYSQ6hBvsLz+cylcZ8hRMKmVy5G1XaMNJID2ah23w== +electron@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-18.3.1.tgz#822ca54bd0a32098712377376617cfdd230846d6" + integrity sha512-46lH3iEdvbbDSa0s2JiOysGruQlJwGUae0UrEfZ4NgHZUnHbglkvezfKSoRSOEob3c9rENZlvgEv9gCbhYx5Yw== dependencies: "@electron/get" "^1.13.0" - "@types/node" "^14.6.2" + "@types/node" "^16.11.26" extract-zip "^1.0.3" encodeurl@^1.0.2: diff --git a/gulp/standalone.js b/gulp/standalone.js index 54a07a11..fab43622 100644 --- a/gulp/standalone.js +++ b/gulp/standalone.js @@ -117,7 +117,7 @@ function gulptasksStandalone($, gulp) { }); gulp.task(taskPrefix + "standalone.prepare.minifyCode", () => { - return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir)); + return gulp.src(path.join(electronBaseDir, "**/*.js")).pipe(gulp.dest(tempDestBuildDir)); }); gulp.task(taskPrefix + "standalone.prepare.copyGamefiles", () => { diff --git a/package.json b/package.json index ef752aac..f9d36e2c 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "babel-plugin-danger-remove-unused-import": "^1.1.2", "css-mqpacker": "^7.0.0", "cssnano": "^4.1.10", + "electron": "^18", "eslint-config-prettier": "6.11.0", "eslint-plugin-prettier": "3.1.3", "faster.js": "^1.1.0", diff --git a/src/js/core/error_handler.js b/src/js/core/error_handler.js index 686e4e4e..fb6e9a25 100644 --- a/src/js/core/error_handler.js +++ b/src/js/core/error_handler.js @@ -120,6 +120,8 @@ function catchErrors(message, source, lineno, colno, error) { }, 200); } + ipcRenderer.send("write-crash-log", (error && error.stack) || message); + return true; } diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 3d0985d5..c611dba8 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -25,7 +25,8 @@ const LOG = createLogger("mods"); * id: string; * minimumGameVersion?: string; * settings: []; - * doesNotAffectSavegame?: boolean + * doesNotAffectSavegame?: boolean; + * filename: string * }} ModMetadata */ @@ -150,6 +151,7 @@ export class ModLoader { LOG.log("hook:init", this.app, this.app.storage); this.exposeExports(); + /** @type {{ filename: string, source: string }[]} */ let mods = []; if (G_IS_STANDALONE) { mods = await ipcRenderer.invoke("get-mods"); @@ -168,11 +170,14 @@ export class ModLoader { "Failed to load " + modURLs[i] + ": " + response.status + " " + response.statusText ); } - mods.push(await response.text()); + mods.push({ + filename: modURLs[i], + source: await response.text(), + }); } } - window.$shapez_registerMod = (modClass, meta) => { + const registerMod = (modFile, modClass, meta) => { if (this.initialized) { throw new Error("Can't register mod after modloader is initialized"); } @@ -180,14 +185,17 @@ export class ModLoader { console.warn("Not registering mod", meta, "since a mod with the same id is already loaded"); return; } + + meta.filename = modFile; this.modLoadQueue.push({ modClass, meta, }); }; - mods.forEach(modCode => { - modCode += ` + mods.forEach(({ filename, source }) => { + window.$shapez_registerMod = registerMod.bind(this, filename); + source += ` if (typeof Mod !== 'undefined') { if (typeof METADATA !== 'object') { throw new Error("No METADATA variable found"); @@ -196,11 +204,11 @@ export class ModLoader { } `; try { - const func = new Function(modCode); + const func = new Function(source); func(); } catch (ex) { console.error(ex); - alert("Failed to parse mod (launch with --dev for more info): \n\n" + ex); + ipcRenderer.send("mod-error", filename, ex); } }); @@ -213,19 +221,16 @@ export class ModLoader { if (meta.minimumGameVersion) { const minimumGameVersion = meta.minimumGameVersion; if (!semverValidRange(minimumGameVersion)) { - alert("Mod " + meta.id + " has invalid minimumGameVersion: " + minimumGameVersion); + const error = new Error(`Invalid minimumGameVersion specified: ${minimumGameVersion}`); + ipcRenderer.send("mod-error", meta.filename, error); continue; } + if (!semverSatisifies(G_BUILD_VERSION, minimumGameVersion)) { - alert( - "Mod '" + - meta.id + - "' is incompatible with this version of the game: \n\n" + - "Mod requires version " + - minimumGameVersion + - " but this game has version " + - G_BUILD_VERSION + const error = new Error( + `This game version (${G_BUILD_VERSION}) is not supported, ${minimumGameVersion} is required` ); + ipcRenderer.send("mod-error", meta.filename, error); continue; } } @@ -258,10 +263,12 @@ export class ModLoader { this.mods.push(mod); } catch (ex) { console.error(ex); - alert("Failed to initialize mods (launch with --dev for more info): \n\n" + ex); + ipcRenderer.send("mod-error", meta.filename, ex); } } + // Once initialization of all mods is done, show all errors + ipcRenderer.send("show-mod-errors"); this.modLoadQueue = []; this.initialized = true; } diff --git a/src/js/states/mods.js b/src/js/states/mods.js index 1e0fe5f1..471eb9ad 100644 --- a/src/js/states/mods.js +++ b/src/js/states/mods.js @@ -126,7 +126,7 @@ export class ModsState extends TextualGameState { this.dialogs.showWarning(T.global.error, T.mods.folderOnlyStandalone); return; } - ipcRenderer.invoke("open-mods-folder"); + ipcRenderer.invoke("open-folder", "mods"); } openBrowseMods() { diff --git a/yarn.lock b/yarn.lock index 27552f93..2291a443 100644 --- a/yarn.lock +++ b/yarn.lock @@ -748,6 +748,22 @@ resolved "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== +"@electron/get@^1.13.0": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.14.1.tgz#16ba75f02dffb74c23965e72d617adc721d27f40" + integrity sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw== + dependencies: + debug "^4.1.1" + env-paths "^2.2.0" + fs-extra "^8.1.0" + got "^9.6.0" + progress "^2.0.3" + semver "^6.2.0" + sumchecker "^3.0.1" + optionalDependencies: + global-agent "^3.0.0" + global-tunnel-ng "^2.7.1" + "@jimp/bmp@^0.6.8": version "0.6.8" resolved "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.6.8.tgz" @@ -1097,11 +1113,23 @@ dependencies: "@types/node" ">= 8" +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz" @@ -1139,6 +1167,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/node@^16.11.26": + version "16.11.36" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.36.tgz#9ab9f8276987132ed2b225cace2218ba794fc751" + integrity sha512-FR5QJe+TaoZ2GsMHkjuwoNabr+UrJNRr2HNOo+r/7vhcuntM6Ee/pRPOnRhhL2XE9OOvX9VLEq+BcXl3VjNoWA== + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz" @@ -1935,6 +1968,11 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +boolean@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -2156,6 +2194,19 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + calipers-gif@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/calipers-gif/-/calipers-gif-2.0.0.tgz" @@ -2435,7 +2486,7 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -clone-response@1.0.2: +clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -2546,7 +2597,7 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.5.0: +concat-stream@^1.5.0, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -3051,6 +3102,11 @@ deep-scope-analyser@^1.7.0: esrecurse "^4.2.1" estraverse "^4.2.0" +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" @@ -3115,6 +3171,11 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz" @@ -3244,6 +3305,15 @@ electron-to-chromium@^1.3.390: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.403.tgz" integrity sha512-JaoxV4RzdBAZOnsF4dAlZ2ijJW72MbqO5lNfOBHUWiBQl3Rwe+mk2RCUMrRI3rSClLJ8HSNQNqcry12H+0ZjFw== +electron@^18: + version "18.3.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-18.3.1.tgz#822ca54bd0a32098712377376617cfdd230846d6" + integrity sha512-46lH3iEdvbbDSa0s2JiOysGruQlJwGUae0UrEfZ4NgHZUnHbglkvezfKSoRSOEob3c9rENZlvgEv9gCbhYx5Yw== + dependencies: + "@electron/get" "^1.13.0" + "@types/node" "^16.11.26" + extract-zip "^1.0.3" + elliptic@^6.0.0, elliptic@^6.5.2: version "6.5.3" resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz" @@ -3282,7 +3352,7 @@ emojis-list@^3.0.0: resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -encodeurl@~1.0.2: +encodeurl@^1.0.2, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= @@ -3317,6 +3387,11 @@ entities@^2.0.0: resolved "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + errno@^0.1.3, errno@~0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz" @@ -3357,6 +3432,11 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es6-error@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== + es6-templates@^0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz" @@ -3375,6 +3455,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-config-prettier@6.11.0: version "6.11.0" resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz" @@ -3685,6 +3770,16 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +extract-zip@^1.0.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== + dependencies: + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" + fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -3965,6 +4060,15 @@ fs-constants@^1.0.0: resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz" @@ -4043,13 +4147,20 @@ get-stream@^2.2.0: object-assign "^4.0.1" pinkie-promise "^2.0.0" -get-stream@^4.0.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz" @@ -4090,6 +4201,18 @@ glob@^7.0.5, glob@^7.0.6, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +global-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" + integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== + dependencies: + boolean "^3.0.1" + es6-error "^4.1.1" + matcher "^3.0.0" + roarr "^2.15.3" + semver "^7.3.2" + serialize-error "^7.0.1" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" @@ -4126,6 +4249,16 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" +global-tunnel-ng@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f" + integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg== + dependencies: + encodeurl "^1.0.2" + lodash "^4.17.10" + npm-conf "^1.1.3" + tunnel "^0.0.6" + global@~4.3.0: version "4.3.2" resolved "https://registry.npmjs.org/global/-/global-4.3.2.tgz" @@ -4151,6 +4284,13 @@ globals@^9.18.0: resolved "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== +globalthis@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + gonzales-pe@^4.2.3: version "4.3.0" resolved "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz" @@ -4201,6 +4341,23 @@ got@^8.3.1: url-parse-lax "^3.0.0" url-to-options "^1.0.1" +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + graceful-fs@^4.1.10, graceful-fs@^4.1.15, graceful-fs@^4.1.2: version "4.2.3" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz" @@ -4211,6 +4368,11 @@ graceful-fs@^4.1.11: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" @@ -4411,6 +4573,11 @@ http-cache-semantics@3.8.1: resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + http-errors@1.7.2, http-errors@~1.7.2: version "1.7.2" resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz" @@ -4974,6 +5141,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz" @@ -4993,6 +5165,13 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + keyv@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz" @@ -5000,6 +5179,13 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" @@ -5172,6 +5358,11 @@ lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17. resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.17.10: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + logalot@^2.0.0, logalot@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz" @@ -5215,11 +5406,16 @@ lowercase-keys@1.0.0: resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz" integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= -lowercase-keys@^1.0.0: +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lpad-align@^1.0.1: version "1.1.2" resolved "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz" @@ -5314,6 +5510,13 @@ match-all@^1.2.5: resolved "https://registry.npmjs.org/match-all/-/match-all-1.2.5.tgz" integrity sha512-KW4trRDMYbVkAKZ1J655vh0931mk3XM1lIJ480TXUL3KBrOsZ6WpryYJELonvtXC1O4erLYB069uHidLkswbjQ== +matcher@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" + integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng== + dependencies: + escape-string-regexp "^4.0.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz" @@ -5443,7 +5646,7 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0: +mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -5482,6 +5685,11 @@ minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz" @@ -5520,6 +5728,13 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz" @@ -5695,7 +5910,12 @@ normalize-url@^3.0.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== -npm-conf@^1.1.0: +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +npm-conf@^1.1.0, npm-conf@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz" integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== @@ -5879,6 +6099,11 @@ p-cancelable@^0.4.0: resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz" @@ -6958,7 +7183,7 @@ process@~0.5.1: resolved "https://registry.npmjs.org/process/-/process-0.5.2.tgz" integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8= -progress@^2.0.0: +progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -7340,7 +7565,7 @@ resolve@^1.10.0, resolve@^1.3.2: dependencies: path-parse "^1.0.6" -responselike@1.0.2: +responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= @@ -7385,6 +7610,18 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +roarr@^2.15.3: + version "2.15.4" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" + integrity sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A== + dependencies: + boolean "^3.0.1" + detect-node "^2.0.4" + globalthis "^1.0.1" + json-stringify-safe "^5.0.1" + semver-compare "^1.0.0" + sprintf-js "^1.1.2" + run-async@^2.4.0: version "2.4.1" resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" @@ -7476,6 +7713,11 @@ seek-bzip@^1.0.5: dependencies: commander "~2.8.1" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + semver-regex@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz" @@ -7498,6 +7740,11 @@ semver@7.0.0: resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + semver@^7.2.1, semver@^7.3.2: version "7.3.2" resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz" @@ -7534,6 +7781,13 @@ serialize-error@^3.0.0: resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-3.0.0.tgz" integrity sha512-+y3nkkG/go1Vdw+2f/+XUXM1DXX1XcxTl99FfiD/OEPUNw4uo0i6FKABfTAN5ZcgGtjTRZcEbxcE/jtXbEY19A== +serialize-error@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" + integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw== + dependencies: + type-fest "^0.13.1" + serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz" @@ -7774,6 +8028,11 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +sprintf-js@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" @@ -8006,6 +8265,13 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +sumchecker@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" + integrity sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg== + dependencies: + debug "^4.1.0" + supports-color@6.1.0, supports-color@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz" @@ -8202,6 +8468,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz" @@ -8283,6 +8554,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -8295,6 +8571,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.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + type-fest@^0.5.1: version "0.5.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz" @@ -8406,6 +8687,11 @@ universal-user-agent@^6.0.0: resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" @@ -8884,7 +9170,7 @@ yarn@^1.22.4: resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.4.tgz" integrity sha512-oYM7hi/lIWm9bCoDMEWgffW8aiNZXCWeZ1/tGy0DWrN6vmzjCXIKu2Y21o8DYVBUtiktwKcNoxyGl/2iKLUNGA== -yauzl@^2.4.2: +yauzl@^2.10.0, yauzl@^2.4.2: version "2.10.0" resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=