this repo has no description

Merge pull request #46 from moonlight-mod/browser

Browser support

authored by

Cynthia Foxwell and committed by
GitHub
ca8adee0 dac42bd4

+1383 -323
+110 -24
build.mjs
··· 13 13 14 14 const prod = process.env.NODE_ENV === "production"; 15 15 const watch = process.argv.includes("--watch"); 16 + const browser = process.argv.includes("--browser"); 17 + const mv2 = process.argv.includes("--mv2"); 16 18 17 19 const external = [ 18 20 "electron", 19 21 "fs", 20 22 "path", 21 23 "module", 22 - "events", 23 - "original-fs", // wtf asar? 24 24 "discord", // mappings 25 25 26 26 // Silence an esbuild warning ··· 74 74 }); 75 75 76 76 async function build(name, entry) { 77 - const outfile = path.join("./dist", name + ".js"); 77 + let outfile = path.join("./dist", name + ".js"); 78 + if (name === "browser") outfile = path.join("./dist", "browser", "index.js"); 78 79 79 80 const dropLabels = []; 80 - if (name !== "injector") dropLabels.push("injector"); 81 - if (name !== "node-preload") dropLabels.push("nodePreload"); 82 - if (name !== "web-preload") dropLabels.push("webPreload"); 81 + const labels = { 82 + injector: ["injector"], 83 + nodePreload: ["node-preload"], 84 + webPreload: ["web-preload"], 85 + browser: ["browser"], 86 + 87 + webTarget: ["web-preload", "browser"], 88 + nodeTarget: ["node-preload", "injector"] 89 + }; 90 + for (const [label, targets] of Object.entries(labels)) { 91 + if (!targets.includes(name)) { 92 + dropLabels.push(label); 93 + } 94 + } 83 95 84 96 const define = { 85 97 MOONLIGHT_ENV: `"${name}"`, 86 98 MOONLIGHT_PROD: prod.toString() 87 99 }; 88 100 89 - for (const iterName of Object.keys(config)) { 101 + for (const iterName of [ 102 + "injector", 103 + "node-preload", 104 + "web-preload", 105 + "browser" 106 + ]) { 90 107 const snake = iterName.replace(/-/g, "_").toUpperCase(); 91 108 define[`MOONLIGHT_${snake}`] = (name === iterName).toString(); 92 109 } 93 110 94 111 const nodeDependencies = ["glob"]; 95 112 const ignoredExternal = name === "web-preload" ? nodeDependencies : []; 113 + 114 + const plugins = [deduplicatedLogging, taggedBuildLog(name)]; 115 + if (name === "browser") { 116 + plugins.push( 117 + copyStaticFiles({ 118 + src: mv2 119 + ? "./packages/browser/manifestv2.json" 120 + : "./packages/browser/manifest.json", 121 + dest: "./dist/browser/manifest.json" 122 + }) 123 + ); 124 + 125 + if (!mv2) { 126 + plugins.push( 127 + copyStaticFiles({ 128 + src: "./packages/browser/modifyResponseHeaders.json", 129 + dest: "./dist/browser/modifyResponseHeaders.json" 130 + }) 131 + ); 132 + plugins.push( 133 + copyStaticFiles({ 134 + src: "./packages/browser/blockLoading.json", 135 + dest: "./dist/browser/blockLoading.json" 136 + }) 137 + ); 138 + } 139 + 140 + plugins.push( 141 + copyStaticFiles({ 142 + src: mv2 143 + ? "./packages/browser/src/background-mv2.js" 144 + : "./packages/browser/src/background.js", 145 + dest: "./dist/browser/background.js" 146 + }) 147 + ); 148 + } 96 149 97 150 /** @type {import("esbuild").BuildOptions} */ 98 151 const esbuildConfig = { ··· 100 153 outfile, 101 154 102 155 format: "cjs", 103 - platform: name === "web-preload" ? "browser" : "node", 156 + platform: ["web-preload", "browser"].includes(name) ? "browser" : "node", 104 157 105 158 treeShaking: true, 106 159 bundle: true, ··· 113 166 dropLabels, 114 167 115 168 logLevel: "silent", 116 - plugins: [deduplicatedLogging, taggedBuildLog(name)] 169 + plugins 117 170 }; 171 + 172 + if (name === "browser") { 173 + const coreExtensionsJson = {}; 174 + 175 + // eslint-disable-next-line no-inner-declarations 176 + function readDir(dir) { 177 + const files = fs.readdirSync(dir); 178 + for (const file of files) { 179 + const filePath = dir + "/" + file; 180 + const normalizedPath = filePath.replace("./dist/core-extensions/", ""); 181 + if (fs.statSync(filePath).isDirectory()) { 182 + readDir(filePath); 183 + } else { 184 + coreExtensionsJson[normalizedPath] = fs.readFileSync( 185 + filePath, 186 + "utf8" 187 + ); 188 + } 189 + } 190 + } 191 + 192 + readDir("./dist/core-extensions"); 193 + 194 + esbuildConfig.banner = { 195 + js: `window._moonlight_coreExtensionsStr = ${JSON.stringify( 196 + JSON.stringify(coreExtensionsJson) 197 + )};` 198 + }; 199 + } 118 200 119 201 if (watch) { 120 202 const ctx = await esbuild.context(esbuildConfig); ··· 211 293 212 294 const promises = []; 213 295 214 - for (const [name, entry] of Object.entries(config)) { 215 - promises.push(build(name, entry)); 216 - } 296 + if (browser) { 297 + build("browser", "packages/browser/src/index.ts"); 298 + } else { 299 + for (const [name, entry] of Object.entries(config)) { 300 + promises.push(build(name, entry)); 301 + } 217 302 218 - const coreExtensions = fs.readdirSync("./packages/core-extensions/src"); 219 - for (const ext of coreExtensions) { 220 - let copiedManifest = false; 303 + const coreExtensions = fs.readdirSync("./packages/core-extensions/src"); 304 + for (const ext of coreExtensions) { 305 + let copiedManifest = false; 221 306 222 - for (const fileExt of ["ts", "tsx"]) { 223 - for (const type of ["index", "node", "host"]) { 224 - if ( 225 - fs.existsSync( 226 - `./packages/core-extensions/src/${ext}/${type}.${fileExt}` 227 - ) 228 - ) { 229 - promises.push(buildExt(ext, type, !copiedManifest, fileExt)); 230 - copiedManifest = true; 307 + for (const fileExt of ["ts", "tsx"]) { 308 + for (const type of ["index", "node", "host"]) { 309 + if ( 310 + fs.existsSync( 311 + `./packages/core-extensions/src/${ext}/${type}.${fileExt}` 312 + ) 313 + ) { 314 + promises.push(buildExt(ext, type, !copiedManifest, fileExt)); 315 + copiedManifest = true; 316 + } 231 317 } 232 318 } 233 319 }
+2
package.json
··· 14 14 "scripts": { 15 15 "build": "node build.mjs", 16 16 "dev": "node build.mjs --watch", 17 + "browser": "node build.mjs --browser", 18 + "browser-mv2": "node build.mjs --browser --mv2", 17 19 "lint": "eslint packages", 18 20 "lint:fix": "eslint packages", 19 21 "lint:report": "eslint --output-file eslint_report.json --format json packages",
+13
packages/browser/blockLoading.json
··· 1 + [ 2 + { 3 + "id": 2, 4 + "priority": 1, 5 + "action": { 6 + "type": "block" 7 + }, 8 + "condition": { 9 + "urlFilter": "*://discord.com/assets/*.js", 10 + "resourceTypes": ["script"] 11 + } 12 + } 13 + ]
+48
packages/browser/manifest.json
··· 1 + { 2 + "manifest_version": 3, 3 + "name": "moonlight", 4 + "description": "Yet another Discord mod", 5 + "version": "1.1.0", 6 + "permissions": [ 7 + "declarativeNetRequestWithHostAccess", 8 + "webRequest", 9 + "scripting", 10 + "webNavigation" 11 + ], 12 + "host_permissions": [ 13 + "https://moonlight-mod.github.io/*", 14 + "https://*.discord.com/*" 15 + ], 16 + "content_scripts": [ 17 + { 18 + "js": ["index.js"], 19 + "matches": ["https://*.discord.com/*"], 20 + "run_at": "document_start", 21 + "world": "MAIN" 22 + } 23 + ], 24 + "declarative_net_request": { 25 + "rule_resources": [ 26 + { 27 + "id": "modifyResponseHeaders", 28 + "enabled": true, 29 + "path": "modifyResponseHeaders.json" 30 + }, 31 + { 32 + "id": "blockLoading", 33 + "enabled": true, 34 + "path": "blockLoading.json" 35 + } 36 + ] 37 + }, 38 + "background": { 39 + "service_worker": "background.js", 40 + "type": "module" 41 + }, 42 + "web_accessible_resources": [ 43 + { 44 + "resources": ["index.js"], 45 + "matches": ["https://*.discord.com/*"] 46 + } 47 + ] 48 + }
+25
packages/browser/manifestv2.json
··· 1 + { 2 + "manifest_version": 2, 3 + "name": "moonlight", 4 + "description": "Yet another Discord mod", 5 + "version": "1.1.0", 6 + "permissions": [ 7 + "webRequest", 8 + "webRequestBlocking", 9 + "scripting", 10 + "webNavigation", 11 + "https://*.discord.com/assets/*.js", 12 + "https://*.discord.com/*" 13 + ], 14 + "background": { 15 + "scripts": ["background.js"] 16 + }, 17 + "content_scripts": [ 18 + { 19 + "js": ["index.js"], 20 + "matches": ["https://*.discord.com/*"], 21 + "run_at": "document_start", 22 + "world": "MAIN" 23 + } 24 + ] 25 + }
+19
packages/browser/modifyResponseHeaders.json
··· 1 + [ 2 + { 3 + "id": 1, 4 + "priority": 2, 5 + "action": { 6 + "type": "modifyHeaders", 7 + "responseHeaders": [ 8 + { 9 + "header": "Content-Security-Policy", 10 + "operation": "remove" 11 + } 12 + ] 13 + }, 14 + "condition": { 15 + "resourceTypes": ["main_frame"], 16 + "initiatorDomains": ["discord.com"] 17 + } 18 + } 19 + ]
+11
packages/browser/package.json
··· 1 + { 2 + "name": "@moonlight-mod/browser", 3 + "private": true, 4 + "dependencies": { 5 + "@moonlight-mod/core": "workspace:*", 6 + "@moonlight-mod/types": "workspace:*", 7 + "@moonlight-mod/web-preload": "workspace:*", 8 + "@zenfs/core": "^1.0.2", 9 + "@zenfs/dom": "^0.2.16" 10 + } 11 + }
+99
packages/browser/src/background-mv2.js
··· 1 + /* eslint-disable no-console */ 2 + /* eslint-disable no-undef */ 3 + 4 + const starterUrls = ["web.", "sentry."]; 5 + let blockLoading = true; 6 + let doing = false; 7 + let collectedUrls = new Set(); 8 + 9 + chrome.webNavigation.onBeforeNavigate.addListener(async (details) => { 10 + const url = new URL(details.url); 11 + if (!blockLoading && url.hostname.endsWith("discord.com")) { 12 + console.log("Blocking", details.url); 13 + blockLoading = true; 14 + collectedUrls.clear(); 15 + } 16 + }); 17 + 18 + async function doTheThing(urls, tabId) { 19 + console.log("Doing", urls, tabId); 20 + 21 + blockLoading = false; 22 + 23 + try { 24 + await chrome.scripting.executeScript({ 25 + target: { tabId }, 26 + world: "MAIN", 27 + args: [urls], 28 + func: async (urls) => { 29 + try { 30 + await window._moonlightBrowserInit(); 31 + } catch (e) { 32 + console.log(e); 33 + } 34 + 35 + const scripts = [...document.querySelectorAll("script")].filter( 36 + (script) => script.src && urls.some((url) => url.includes(script.src)) 37 + ); 38 + 39 + // backwards 40 + urls.reverse(); 41 + for (const url of urls) { 42 + const script = scripts.find((script) => url.includes(script.src)); 43 + console.log("adding new script", script); 44 + 45 + const newScript = document.createElement("script"); 46 + for (const { name, value } of script.attributes) { 47 + newScript.setAttribute(name, value); 48 + } 49 + 50 + script.remove(); 51 + document.documentElement.appendChild(newScript); 52 + } 53 + } 54 + }); 55 + } catch (e) { 56 + console.log(e); 57 + } 58 + 59 + doing = false; 60 + collectedUrls.clear(); 61 + } 62 + 63 + chrome.webRequest.onBeforeRequest.addListener( 64 + async (details) => { 65 + if (starterUrls.some((url) => details.url.includes(url))) { 66 + console.log("Adding", details.url); 67 + collectedUrls.add(details.url); 68 + } 69 + 70 + if (collectedUrls.size === starterUrls.length) { 71 + if (doing) return; 72 + if (!blockLoading) return; 73 + doing = true; 74 + const urls = [...collectedUrls]; 75 + const tabId = details.tabId; 76 + 77 + // yes this is a load-bearing sleep 78 + setTimeout(() => doTheThing(urls, tabId), 0); 79 + } 80 + 81 + if (blockLoading) return { cancel: true }; 82 + }, 83 + { 84 + urls: ["https://*.discord.com/assets/*.js"] 85 + }, 86 + ["blocking"] 87 + ); 88 + 89 + chrome.webRequest.onHeadersReceived.addListener( 90 + (details) => { 91 + return { 92 + responseHeaders: details.responseHeaders.filter( 93 + (header) => header.name.toLowerCase() !== "content-security-policy" 94 + ) 95 + }; 96 + }, 97 + { urls: ["https://*.discord.com/*"] }, 98 + ["blocking", "responseHeaders"] 99 + );
+114
packages/browser/src/background.js
··· 1 + /* eslint-disable no-console */ 2 + /* eslint-disable no-undef */ 3 + 4 + const starterUrls = ["web.", "sentry."]; 5 + let blockLoading = true; 6 + let doing = false; 7 + let collectedUrls = new Set(); 8 + 9 + chrome.webNavigation.onBeforeNavigate.addListener(async (details) => { 10 + const url = new URL(details.url); 11 + if (!blockLoading && url.hostname.endsWith("discord.com")) { 12 + await chrome.declarativeNetRequest.updateEnabledRulesets({ 13 + enableRulesetIds: ["modifyResponseHeaders", "blockLoading"] 14 + }); 15 + blockLoading = true; 16 + collectedUrls.clear(); 17 + } 18 + }); 19 + 20 + chrome.webRequest.onBeforeRequest.addListener( 21 + async (details) => { 22 + if (details.tabId === -1) return; 23 + if (starterUrls.some((url) => details.url.includes(url))) { 24 + console.log("Adding", details.url); 25 + collectedUrls.add(details.url); 26 + } 27 + 28 + if (collectedUrls.size === starterUrls.length) { 29 + if (doing) return; 30 + if (!blockLoading) return; 31 + doing = true; 32 + const urls = [...collectedUrls]; 33 + console.log("Doing", urls); 34 + 35 + console.log("Running moonlight script"); 36 + try { 37 + await chrome.scripting.executeScript({ 38 + target: { tabId: details.tabId }, 39 + world: "MAIN", 40 + files: ["index.js"] 41 + }); 42 + } catch (e) { 43 + console.log(e); 44 + } 45 + 46 + console.log("Initializing moonlight"); 47 + try { 48 + await chrome.scripting.executeScript({ 49 + target: { tabId: details.tabId }, 50 + world: "MAIN", 51 + func: async () => { 52 + try { 53 + await window._moonlightBrowserInit(); 54 + } catch (e) { 55 + console.log(e); 56 + } 57 + } 58 + }); 59 + } catch (e) { 60 + console.log(e); 61 + } 62 + 63 + console.log("Updating rulesets"); 64 + try { 65 + blockLoading = false; 66 + await chrome.declarativeNetRequest.updateEnabledRulesets({ 67 + disableRulesetIds: ["blockLoading"], 68 + enableRulesetIds: ["modifyResponseHeaders"] 69 + }); 70 + } catch (e) { 71 + console.log(e); 72 + } 73 + 74 + console.log("Readding scripts"); 75 + try { 76 + await chrome.scripting.executeScript({ 77 + target: { tabId: details.tabId }, 78 + world: "MAIN", 79 + args: [urls], 80 + func: async (urls) => { 81 + const scripts = [...document.querySelectorAll("script")].filter( 82 + (script) => 83 + script.src && urls.some((url) => url.includes(script.src)) 84 + ); 85 + 86 + // backwards 87 + urls.reverse(); 88 + for (const url of urls) { 89 + const script = scripts.find((script) => url.includes(script.src)); 90 + console.log("adding new script", script); 91 + 92 + const newScript = document.createElement("script"); 93 + for (const { name, value } of script.attributes) { 94 + newScript.setAttribute(name, value); 95 + } 96 + 97 + script.remove(); 98 + document.documentElement.appendChild(newScript); 99 + } 100 + } 101 + }); 102 + } catch (e) { 103 + console.log(e); 104 + } 105 + 106 + console.log("Done"); 107 + doing = false; 108 + collectedUrls.clear(); 109 + } 110 + }, 111 + { 112 + urls: ["*://*.discord.com/assets/*.js"] 113 + } 114 + );
+132
packages/browser/src/index.ts
··· 1 + import "@moonlight-mod/web-preload"; 2 + import { readConfig, writeConfig } from "@moonlight-mod/core/config"; 3 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 4 + import { getExtensions } from "@moonlight-mod/core/extension"; 5 + import { loadExtensions } from "@moonlight-mod/core/extension/loader"; 6 + import { MoonlightBrowserFS, MoonlightNode } from "@moonlight-mod/types"; 7 + import { IndexedDB } from "@zenfs/dom"; 8 + import { configure } from "@zenfs/core"; 9 + import * as fs from "@zenfs/core/promises"; 10 + 11 + window._moonlightBrowserInit = async () => { 12 + // Set up a virtual filesystem with IndexedDB 13 + await configure({ 14 + mounts: { 15 + "/": { 16 + backend: IndexedDB, 17 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment 18 + // @ts-ignore tsc tweaking 19 + storeName: "moonlight-fs" 20 + } 21 + } 22 + }); 23 + 24 + const browserFS: MoonlightBrowserFS = { 25 + async readFile(path) { 26 + return new Uint8Array(await fs.readFile(path)); 27 + }, 28 + async writeFile(path, data) { 29 + await fs.writeFile(path, data); 30 + }, 31 + async unlink(path) { 32 + await fs.unlink(path); 33 + }, 34 + 35 + async readdir(path) { 36 + return await fs.readdir(path); 37 + }, 38 + async mkdir(path) { 39 + const parts = this.parts(path); 40 + for (let i = 0; i < parts.length; i++) { 41 + const path = this.join(...parts.slice(0, i + 1)); 42 + if (!(await this.exists(path))) await fs.mkdir(path); 43 + } 44 + }, 45 + 46 + async rmdir(path) { 47 + const entries = await this.readdir(path); 48 + 49 + for (const entry of entries) { 50 + const fullPath = this.join(path, entry); 51 + const isFile = await this.isFile(fullPath); 52 + if (isFile) { 53 + await this.unlink(fullPath); 54 + } else { 55 + await this.rmdir(fullPath); 56 + } 57 + } 58 + 59 + await fs.rmdir(path); 60 + }, 61 + 62 + async exists(path) { 63 + return await fs.exists(path); 64 + }, 65 + async isFile(path) { 66 + return (await fs.stat(path)).isFile(); 67 + }, 68 + 69 + join(...parts) { 70 + let str = parts.join("/"); 71 + if (!str.startsWith("/")) str = "/" + str; 72 + return str; 73 + }, 74 + dirname(path) { 75 + const parts = this.parts(path); 76 + return "/" + parts.slice(0, parts.length - 1).join("/"); 77 + }, 78 + parts(path) { 79 + if (path.startsWith("/")) path = path.substring(1); 80 + return path.split("/"); 81 + } 82 + }; 83 + Object.assign(window, { 84 + _moonlightBrowserFS: browserFS 85 + }); 86 + 87 + // Actual loading begins here 88 + const config = await readConfig(); 89 + initLogger(config); 90 + 91 + const extensions = await getExtensions(); 92 + const processedExtensions = await loadExtensions(extensions); 93 + 94 + function getConfig(ext: string) { 95 + const val = config.extensions[ext]; 96 + if (val == null || typeof val === "boolean") return undefined; 97 + return val.config; 98 + } 99 + 100 + const moonlightNode: MoonlightNode = { 101 + config, 102 + extensions, 103 + processedExtensions, 104 + nativesCache: {}, 105 + 106 + getConfig, 107 + getConfigOption: <T>(ext: string, name: string) => { 108 + const config = getConfig(ext); 109 + if (config == null) return undefined; 110 + const option = config[name]; 111 + if (option == null) return undefined; 112 + return option as T; 113 + }, 114 + getNatives: () => {}, 115 + getLogger: (id: string) => { 116 + return new Logger(id); 117 + }, 118 + 119 + getExtensionDir: (ext: string) => { 120 + return `/extensions/${ext}`; 121 + }, 122 + 123 + writeConfig 124 + }; 125 + 126 + Object.assign(window, { 127 + moonlightNode 128 + }); 129 + 130 + // This is set by web-preload for us 131 + await window._moonlightBrowserLoad(); 132 + };
+6
packages/browser/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "compilerOptions": { 4 + "module": "ES2022" 5 + } 6 + }
+2 -2
packages/core-extensions/package.json
··· 2 2 "name": "@moonlight-mod/core-extensions", 3 3 "private": true, 4 4 "dependencies": { 5 - "@electron/asar": "^3.2.5", 6 - "@moonlight-mod/types": "workspace:*" 5 + "@moonlight-mod/types": "workspace:*", 6 + "@moonlight-mod/core": "workspace:*" 7 7 } 8 8 }
+11 -8
packages/core-extensions/src/moonbase/node.ts
··· 1 1 import { MoonbaseNatives, RepositoryManifest } from "./types"; 2 - import asar from "@electron/asar"; 3 2 import fs from "fs"; 4 3 import path from "path"; 5 - import os from "os"; 6 - import { repoUrlFile } from "types/src/constants"; 4 + import extractAsar from "@moonlight-mod/core/asar"; 5 + import { repoUrlFile } from "@moonlight-mod/types/constants"; 7 6 8 7 const logger = moonlightNode.getLogger("moonbase"); 9 8 ··· 35 34 if (fs.existsSync(dir)) fs.rmdirSync(dir, { recursive: true }); 36 35 fs.mkdirSync(dir, { recursive: true }); 37 36 38 - // for some reason i just can't .writeFileSync() a file that ends in .asar??? 39 - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "moonlight-")); 40 - const tempFile = path.join(tempDir, "extension"); 41 37 const buffer = await req.arrayBuffer(); 42 - fs.writeFileSync(tempFile, Buffer.from(buffer)); 38 + const files = extractAsar(buffer); 39 + for (const [file, buf] of Object.entries(files)) { 40 + const nodeBuf = Buffer.from(buf); 41 + const fullFile = path.join(dir, file); 42 + const fullDir = path.dirname(fullFile); 43 43 44 - asar.extractAll(tempFile, dir); 44 + if (!fs.existsSync(fullDir)) fs.mkdirSync(fullDir, { recursive: true }); 45 + fs.writeFileSync(path.join(dir, file), nodeBuf); 46 + } 47 + 45 48 fs.writeFileSync(path.join(dir, repoUrlFile), repo); 46 49 } 47 50
+67 -5
packages/core-extensions/src/moonbase/webpackModules/stores.ts
··· 1 1 import { Config, ExtensionLoadSource } from "@moonlight-mod/types"; 2 - import { ExtensionState, MoonbaseExtension, MoonbaseNatives } from "../types"; 2 + import { 3 + ExtensionState, 4 + MoonbaseExtension, 5 + MoonbaseNatives, 6 + RepositoryManifest 7 + } from "../types"; 3 8 import { Store } from "@moonlight-mod/wp/discord/packages/flux"; 4 9 import Dispatcher from "@moonlight-mod/wp/discord/Dispatcher"; 10 + import extractAsar from "@moonlight-mod/core/asar"; 11 + import { repoUrlFile } from "@moonlight-mod/types/constants"; 5 12 6 - const natives: MoonbaseNatives = moonlight.getNatives("moonbase"); 7 13 const logger = moonlight.getLogger("moonbase"); 8 14 15 + let natives: MoonbaseNatives = moonlight.getNatives("moonbase"); 16 + if (window._moonlightBrowserFS != null) { 17 + const browserFS = window._moonlightBrowserFS!; 18 + natives = { 19 + fetchRepositories: async (repos) => { 20 + const ret: Record<string, RepositoryManifest[]> = {}; 21 + 22 + for (const repo of repos) { 23 + try { 24 + const req = await fetch(repo); 25 + const json = await req.json(); 26 + ret[repo] = json; 27 + } catch (e) { 28 + logger.error(`Error fetching repository ${repo}`, e); 29 + } 30 + } 31 + 32 + return ret; 33 + }, 34 + installExtension: async (manifest, url, repo) => { 35 + const req = await fetch(url); 36 + const buffer = await req.arrayBuffer(); 37 + 38 + if (await browserFS.exists("/extensions/" + manifest.id)) { 39 + await browserFS.rmdir("/extensions/" + manifest.id); 40 + } 41 + 42 + const files = extractAsar(buffer); 43 + for (const [file, data] of Object.entries(files)) { 44 + const path = 45 + "/extensions/" + 46 + manifest.id + 47 + (file.startsWith("/") ? file : `/${file}`); 48 + await browserFS.mkdir(browserFS.dirname(path)); 49 + await browserFS.writeFile(path, data); 50 + } 51 + 52 + await browserFS.writeFile( 53 + `/extensions/${manifest.id}/` + repoUrlFile, 54 + new TextEncoder().encode(repo) 55 + ); 56 + }, 57 + deleteExtension: async (id) => { 58 + browserFS.rmdir("/extensions/" + id); 59 + }, 60 + getExtensionConfig: (id, key) => { 61 + const config = moonlightNode.config.extensions[id]; 62 + if (typeof config === "object") { 63 + return config.config?.[key]; 64 + } 65 + 66 + return undefined; 67 + } 68 + }; 69 + } 70 + 9 71 class MoonbaseSettingsStore extends Store<any> { 10 72 private origConfig: Config; 11 73 private config: Config; ··· 42 104 }; 43 105 } 44 106 45 - natives.fetchRepositories(this.config.repositories).then((ret) => { 107 + natives!.fetchRepositories(this.config.repositories).then((ret) => { 46 108 for (const [repo, exts] of Object.entries(ret)) { 47 109 try { 48 110 for (const ext of exts) { ··· 220 282 this.installing = true; 221 283 try { 222 284 const url = this.updates[uniqueId]?.download ?? ext.manifest.download; 223 - await natives.installExtension(ext.manifest, url, ext.source.url!); 285 + await natives!.installExtension(ext.manifest, url, ext.source.url!); 224 286 if (ext.state === ExtensionState.NotDownloaded) { 225 287 this.extensions[uniqueId].state = ExtensionState.Disabled; 226 288 } ··· 240 302 241 303 this.installing = true; 242 304 try { 243 - await natives.deleteExtension(ext.id); 305 + await natives!.deleteExtension(ext.id); 244 306 this.extensions[uniqueId].state = ExtensionState.NotDownloaded; 245 307 } catch (e) { 246 308 logger.error("Error deleting extension:", e);
+57
packages/core/src/asar.ts
··· 1 + // https://github.com/electron/asar 2 + // http://formats.kaitai.io/python_pickle/ 3 + import { BinaryReader } from "./util/binary"; 4 + 5 + /* 6 + The asar format is kinda bad, especially because it uses multiple pickle 7 + entries. It spams sizes, expecting us to read small buffers and parse those, 8 + but we can just take it all through at once without having to create multiple 9 + BinaryReaders. This implementation might be wrong, though. 10 + 11 + This either has size/offset or files but I can't get the type to cooperate, 12 + so pretend this is a union. 13 + */ 14 + 15 + type AsarEntry = { 16 + size: number; 17 + offset: `${number}`; // who designed this 18 + 19 + files?: Record<string, AsarEntry>; 20 + }; 21 + 22 + export default function extractAsar(file: ArrayBuffer) { 23 + const array = new Uint8Array(file); 24 + const br = new BinaryReader(array); 25 + 26 + // two uints, one containing the number '4', to signify that the other uint takes up 4 bytes 27 + // bravo, electron, bravo 28 + const _payloadSize = br.readUInt32(); 29 + const _headerSize = br.readInt32(); 30 + 31 + const headerStringStart = br.position; 32 + const headerStringSize = br.readUInt32(); // How big the block is 33 + const actualStringSize = br.readUInt32(); // How big the string in that block is 34 + 35 + const base = headerStringStart + headerStringSize + 4; 36 + 37 + const string = br.readString(actualStringSize); 38 + const header: AsarEntry = JSON.parse(string); 39 + 40 + const ret: Record<string, Uint8Array> = {}; 41 + function addDirectory(dir: AsarEntry, path: string) { 42 + for (const [name, data] of Object.entries(dir.files!)) { 43 + const fullName = path + "/" + name; 44 + if (data.files != null) { 45 + addDirectory(data, fullName); 46 + } else { 47 + br.position = base + parseInt(data.offset); 48 + const file = br.read(data.size); 49 + ret[fullName] = file; 50 + } 51 + } 52 + } 53 + 54 + addDirectory(header, ""); 55 + 56 + return ret; 57 + }
+42 -23
packages/core/src/config.ts
··· 13 13 }; 14 14 15 15 export function writeConfig(config: Config) { 16 - const fs = requireImport("fs"); 17 - const configPath = getConfigPath(); 18 - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 19 - } 16 + browser: { 17 + const enc = new TextEncoder().encode(JSON.stringify(config, null, 2)); 18 + window._moonlightBrowserFS!.writeFile("/config.json", enc); 19 + return; 20 + } 20 21 21 - function readConfigNode(): Config { 22 - const fs = requireImport("fs"); 23 - const configPath = getConfigPath(); 24 - 25 - if (!fs.existsSync(configPath)) { 26 - writeConfig(defaultConfig); 27 - return defaultConfig; 22 + nodeTarget: { 23 + const fs = requireImport("fs"); 24 + const configPath = getConfigPath(); 25 + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 26 + return; 28 27 } 29 28 30 - let config: Config = JSON.parse(fs.readFileSync(configPath, "utf8")); 31 - 32 - // Assign the default values if they don't exist (newly added) 33 - config = { ...defaultConfig, ...config }; 34 - writeConfig(config); 35 - 36 - return config; 29 + throw new Error("Called writeConfig() in an impossible environment"); 37 30 } 38 31 39 - export function readConfig(): Config { 32 + export async function readConfig(): Promise<Config> { 40 33 webPreload: { 41 34 return moonlightNode.config; 42 35 } 43 36 44 - nodePreload: { 45 - return readConfigNode(); 37 + browser: { 38 + if (await window._moonlightBrowserFS!.exists("/config.json")) { 39 + const file = await window._moonlightBrowserFS!.readFile("/config.json"); 40 + const configStr = new TextDecoder().decode(file); 41 + let config: Config = JSON.parse(configStr); 42 + 43 + config = { ...defaultConfig, ...config }; 44 + writeConfig(config); 45 + 46 + return config; 47 + } else { 48 + writeConfig(defaultConfig); 49 + return defaultConfig; 50 + } 46 51 } 47 52 48 - injector: { 49 - return readConfigNode(); 53 + nodeTarget: { 54 + const fs = requireImport("fs"); 55 + const configPath = getConfigPath(); 56 + 57 + if (!fs.existsSync(configPath)) { 58 + writeConfig(defaultConfig); 59 + return defaultConfig; 60 + } 61 + 62 + let config: Config = JSON.parse(fs.readFileSync(configPath, "utf8")); 63 + 64 + // Assign the default values if they don't exist (newly added) 65 + config = { ...defaultConfig, ...config }; 66 + writeConfig(config); 67 + 68 + return config; 50 69 } 51 70 52 71 throw new Error("Called readConfig() in an impossible environment");
+206 -75
packages/core/src/extension.ts
··· 7 7 import { readConfig } from "./config"; 8 8 import requireImport from "./util/import"; 9 9 import { getCoreExtensionsPath, getExtensionsPath } from "./util/data"; 10 + import Logger from "./util/logger"; 10 11 11 - function findManifests(dir: string): string[] { 12 + const logger = new Logger("core/extension"); 13 + 14 + // This is kinda duplicated from the browser FS type but idc 15 + interface MoonlightFSWrapper { 16 + readdir(path: string): Promise<string[]>; 17 + exists(path: string): Promise<boolean>; 18 + isFile(path: string): Promise<boolean>; 19 + readFile(path: string): Promise<string>; 20 + join(...parts: string[]): string; 21 + dirname(path: string): string; 22 + } 23 + 24 + function getFS(): MoonlightFSWrapper { 25 + browser: { 26 + const fs = window._moonlightBrowserFS!; 27 + return { 28 + async readdir(path) { 29 + return await fs.readdir(path); 30 + }, 31 + async exists(path) { 32 + return await fs.exists(path); 33 + }, 34 + async isFile(path) { 35 + return await fs.isFile(path); 36 + }, 37 + async readFile(path) { 38 + const buf = await fs.readFile(path); 39 + const text = new TextDecoder().decode(buf); 40 + return text; 41 + }, 42 + join(...parts) { 43 + return fs.join(...parts); 44 + }, 45 + dirname(path) { 46 + return fs.dirname(path); 47 + } 48 + }; 49 + } 50 + 12 51 const fs = requireImport("fs"); 13 52 const path = requireImport("path"); 53 + 54 + return { 55 + async readdir(path) { 56 + return fs.readdirSync(path); 57 + }, 58 + async exists(path) { 59 + return fs.existsSync(path); 60 + }, 61 + async isFile(path) { 62 + return fs.statSync(path).isFile(); 63 + }, 64 + async readFile(path) { 65 + return fs.readFileSync(path, "utf8"); 66 + }, 67 + join(...parts) { 68 + return path.join(...parts); 69 + }, 70 + dirname(dir) { 71 + return path.dirname(dir); 72 + } 73 + }; 74 + } 75 + 76 + async function findManifests( 77 + fs: MoonlightFSWrapper, 78 + dir: string 79 + ): Promise<string[]> { 14 80 const ret = []; 15 81 16 - if (fs.existsSync(dir)) { 17 - for (const file of fs.readdirSync(dir)) { 82 + if (await fs.exists(dir)) { 83 + for (const file of await fs.readdir(dir)) { 84 + const path = fs.join(dir, file); 18 85 if (file === "manifest.json") { 19 - ret.push(path.join(dir, file)); 86 + ret.push(path); 20 87 } 21 88 22 - if (fs.statSync(path.join(dir, file)).isDirectory()) { 23 - ret.push(...findManifests(path.join(dir, file))); 89 + if (!(await fs.isFile(path))) { 90 + ret.push(...(await findManifests(fs, path))); 24 91 } 25 92 } 26 93 } ··· 28 95 return ret; 29 96 } 30 97 31 - function loadDetectedExtensions( 98 + async function loadDetectedExtensions( 99 + fs: MoonlightFSWrapper, 32 100 dir: string, 33 101 type: ExtensionLoadSource 34 - ): DetectedExtension[] { 35 - const fs = requireImport("fs"); 36 - const path = requireImport("path"); 102 + ): Promise<DetectedExtension[]> { 37 103 const ret: DetectedExtension[] = []; 38 104 39 - const manifests = findManifests(dir); 105 + const manifests = await findManifests(fs, dir); 40 106 for (const manifestPath of manifests) { 41 - if (!fs.existsSync(manifestPath)) continue; 42 - const dir = path.dirname(manifestPath); 107 + try { 108 + if (!(await fs.exists(manifestPath))) continue; 109 + const dir = fs.dirname(manifestPath); 43 110 44 - const manifest: ExtensionManifest = JSON.parse( 45 - fs.readFileSync(manifestPath, "utf8") 46 - ); 47 - const level = manifest.apiLevel ?? 1; 48 - if (level !== constants.apiLevel) { 49 - continue; 50 - } 111 + const manifest: ExtensionManifest = JSON.parse( 112 + await fs.readFile(manifestPath) 113 + ); 114 + const level = manifest.apiLevel ?? 1; 115 + if (level !== constants.apiLevel) { 116 + continue; 117 + } 51 118 52 - const webPath = path.join(dir, "index.js"); 53 - const nodePath = path.join(dir, "node.js"); 54 - const hostPath = path.join(dir, "host.js"); 119 + const webPath = fs.join(dir, "index.js"); 120 + const nodePath = fs.join(dir, "node.js"); 121 + const hostPath = fs.join(dir, "host.js"); 55 122 56 - // if none exist (empty manifest) don't give a shit 57 - if ( 58 - !fs.existsSync(webPath) && 59 - !fs.existsSync(nodePath) && 60 - !fs.existsSync(hostPath) 61 - ) { 62 - continue; 63 - } 123 + // if none exist (empty manifest) don't give a shit 124 + if (!fs.exists(webPath) && !fs.exists(nodePath) && !fs.exists(hostPath)) { 125 + continue; 126 + } 64 127 65 - const web = fs.existsSync(webPath) 66 - ? fs.readFileSync(webPath, "utf8") 67 - : undefined; 128 + const web = (await fs.exists(webPath)) 129 + ? await fs.readFile(webPath) 130 + : undefined; 68 131 69 - let url: string | undefined = undefined; 70 - const urlPath = path.join(dir, constants.repoUrlFile); 71 - if (type === ExtensionLoadSource.Normal && fs.existsSync(urlPath)) { 72 - url = fs.readFileSync(urlPath, "utf8"); 73 - } 132 + let url: string | undefined = undefined; 133 + const urlPath = fs.join(dir, constants.repoUrlFile); 134 + if (type === ExtensionLoadSource.Normal && (await fs.exists(urlPath))) { 135 + url = await fs.readFile(urlPath); 136 + } 74 137 75 - const wpModules: Record<string, string> = {}; 76 - const wpModulesPath = path.join(dir, "webpackModules"); 77 - if (fs.existsSync(wpModulesPath)) { 78 - const wpModulesFile = fs.readdirSync(wpModulesPath); 138 + const wpModules: Record<string, string> = {}; 139 + const wpModulesPath = fs.join(dir, "webpackModules"); 140 + if (await fs.exists(wpModulesPath)) { 141 + const wpModulesFile = await fs.readdir(wpModulesPath); 79 142 80 - for (const wpModuleFile of wpModulesFile) { 81 - if (wpModuleFile.endsWith(".js")) { 82 - wpModules[wpModuleFile.replace(".js", "")] = fs.readFileSync( 83 - path.join(wpModulesPath, wpModuleFile), 84 - "utf8" 85 - ); 143 + for (const wpModuleFile of wpModulesFile) { 144 + if (wpModuleFile.endsWith(".js")) { 145 + wpModules[wpModuleFile.replace(".js", "")] = await fs.readFile( 146 + fs.join(wpModulesPath, wpModuleFile) 147 + ); 148 + } 86 149 } 87 150 } 88 - } 89 151 90 - ret.push({ 91 - id: manifest.id, 92 - manifest, 93 - source: { 94 - type, 95 - url 96 - }, 97 - scripts: { 98 - web, 99 - webPath: web != null ? webPath : undefined, 100 - webpackModules: wpModules, 101 - nodePath: fs.existsSync(nodePath) ? nodePath : undefined, 102 - hostPath: fs.existsSync(hostPath) ? hostPath : undefined 103 - } 104 - }); 152 + ret.push({ 153 + id: manifest.id, 154 + manifest, 155 + source: { 156 + type, 157 + url 158 + }, 159 + scripts: { 160 + web, 161 + webPath: web != null ? webPath : undefined, 162 + webpackModules: wpModules, 163 + nodePath: (await fs.exists(nodePath)) ? nodePath : undefined, 164 + hostPath: (await fs.exists(hostPath)) ? hostPath : undefined 165 + } 166 + }); 167 + } catch (e) { 168 + logger.error(e, "Failed to load extension"); 169 + } 105 170 } 106 171 107 172 return ret; 108 173 } 109 174 110 - function getExtensionsNative(): DetectedExtension[] { 111 - const config = readConfig(); 175 + async function getExtensionsNative(): Promise<DetectedExtension[]> { 176 + const config = await readConfig(); 112 177 const res = []; 178 + const fs = getFS(); 113 179 114 180 res.push( 115 - ...loadDetectedExtensions(getCoreExtensionsPath(), ExtensionLoadSource.Core) 181 + ...(await loadDetectedExtensions( 182 + fs, 183 + getCoreExtensionsPath(), 184 + ExtensionLoadSource.Core 185 + )) 116 186 ); 117 187 118 188 res.push( 119 - ...loadDetectedExtensions(getExtensionsPath(), ExtensionLoadSource.Normal) 189 + ...(await loadDetectedExtensions( 190 + fs, 191 + getExtensionsPath(), 192 + ExtensionLoadSource.Normal 193 + )) 120 194 ); 121 195 122 196 for (const devSearchPath of config.devSearchPaths ?? []) { 123 197 res.push( 124 - ...loadDetectedExtensions(devSearchPath, ExtensionLoadSource.Developer) 198 + ...(await loadDetectedExtensions( 199 + fs, 200 + devSearchPath, 201 + ExtensionLoadSource.Developer 202 + )) 125 203 ); 126 204 } 127 205 128 206 return res; 129 207 } 130 208 131 - export function getExtensions(): DetectedExtension[] { 209 + async function getExtensionsBrowser(): Promise<DetectedExtension[]> { 210 + const ret: DetectedExtension[] = []; 211 + 212 + const coreExtensionsFs: Record<string, string> = JSON.parse( 213 + // @ts-expect-error shut up 214 + _moonlight_coreExtensionsStr 215 + ); 216 + const coreExtensions = Array.from( 217 + new Set(Object.keys(coreExtensionsFs).map((x) => x.split("/")[0])) 218 + ); 219 + 220 + for (const ext of coreExtensions) { 221 + if (!coreExtensionsFs[`${ext}/index.js`]) continue; 222 + const manifest = JSON.parse(coreExtensionsFs[`${ext}/manifest.json`]); 223 + const web = coreExtensionsFs[`${ext}/index.js`]; 224 + 225 + const wpModules: Record<string, string> = {}; 226 + const wpModulesPath = `${ext}/webpackModules`; 227 + for (const wpModuleFile of Object.keys(coreExtensionsFs)) { 228 + if (wpModuleFile.startsWith(wpModulesPath)) { 229 + wpModules[ 230 + wpModuleFile.replace(wpModulesPath + "/", "").replace(".js", "") 231 + ] = coreExtensionsFs[wpModuleFile]; 232 + } 233 + } 234 + 235 + ret.push({ 236 + id: manifest.id, 237 + manifest, 238 + source: { 239 + type: ExtensionLoadSource.Core 240 + }, 241 + scripts: { 242 + web, 243 + webpackModules: wpModules 244 + } 245 + }); 246 + } 247 + 248 + const fs = getFS(); 249 + if (await fs.exists("/extensions")) { 250 + ret.push( 251 + ...(await loadDetectedExtensions( 252 + fs, 253 + "/extensions", 254 + ExtensionLoadSource.Normal 255 + )) 256 + ); 257 + } 258 + 259 + return ret; 260 + } 261 + 262 + export async function getExtensions(): Promise<DetectedExtension[]> { 132 263 webPreload: { 133 264 return moonlightNode.extensions; 134 265 } 135 266 136 - nodePreload: { 137 - return getExtensionsNative(); 267 + browser: { 268 + return await getExtensionsBrowser(); 138 269 } 139 270 140 - injector: { 141 - return getExtensionsNative(); 271 + nodeTarget: { 272 + return await getExtensionsNative(); 142 273 } 143 274 144 275 throw new Error("Called getExtensions() outside of node-preload/web-preload");
+56 -52
packages/core/src/extension/loader.ts
··· 14 14 15 15 const logger = new Logger("core/extension/loader"); 16 16 17 - async function loadExt(ext: DetectedExtension) { 18 - webPreload: { 19 - if (ext.scripts.web != null) { 20 - const source = ext.scripts.web; 21 - const fn = new Function("require", "module", "exports", source); 17 + function loadExtWeb(ext: DetectedExtension) { 18 + if (ext.scripts.web != null) { 19 + const source = ext.scripts.web; 20 + const fn = new Function("require", "module", "exports", source); 22 21 23 - const module = { id: ext.id, exports: {} }; 24 - fn.apply(window, [ 25 - () => { 26 - logger.warn("Attempted to require() from web"); 27 - }, 28 - module, 29 - module.exports 30 - ]); 22 + const module = { id: ext.id, exports: {} }; 23 + fn.apply(window, [ 24 + () => { 25 + logger.warn("Attempted to require() from web"); 26 + }, 27 + module, 28 + module.exports 29 + ]); 31 30 32 - const exports: ExtensionWebExports = module.exports; 33 - if (exports.patches != null) { 34 - let idx = 0; 35 - for (const patch of exports.patches) { 36 - if (Array.isArray(patch.replace)) { 37 - for (const replacement of patch.replace) { 38 - const newPatch = Object.assign({}, patch, { 39 - replace: replacement 40 - }); 31 + const exports: ExtensionWebExports = module.exports; 32 + if (exports.patches != null) { 33 + let idx = 0; 34 + for (const patch of exports.patches) { 35 + if (Array.isArray(patch.replace)) { 36 + for (const replacement of patch.replace) { 37 + const newPatch = Object.assign({}, patch, { 38 + replace: replacement 39 + }); 41 40 42 - registerPatch({ ...newPatch, ext: ext.id, id: idx }); 43 - idx++; 44 - } 45 - } else { 46 - registerPatch({ ...patch, ext: ext.id, id: idx }); 41 + registerPatch({ ...newPatch, ext: ext.id, id: idx }); 47 42 idx++; 48 43 } 44 + } else { 45 + registerPatch({ ...patch, ext: ext.id, id: idx }); 46 + idx++; 49 47 } 50 48 } 49 + } 51 50 52 - if (exports.webpackModules != null) { 53 - for (const [name, wp] of Object.entries(exports.webpackModules)) { 54 - if (wp.run == null && ext.scripts.webpackModules?.[name] != null) { 55 - const func = new Function( 56 - "module", 57 - "exports", 58 - "require", 59 - ext.scripts.webpackModules[name]! 60 - ) as WebpackModuleFunc; 61 - registerWebpackModule({ 62 - ...wp, 63 - ext: ext.id, 64 - id: name, 65 - run: func 66 - }); 67 - } else { 68 - registerWebpackModule({ ...wp, ext: ext.id, id: name }); 69 - } 51 + if (exports.webpackModules != null) { 52 + for (const [name, wp] of Object.entries(exports.webpackModules)) { 53 + if (wp.run == null && ext.scripts.webpackModules?.[name] != null) { 54 + const func = new Function( 55 + "module", 56 + "exports", 57 + "require", 58 + ext.scripts.webpackModules[name]! 59 + ) as WebpackModuleFunc; 60 + registerWebpackModule({ 61 + ...wp, 62 + ext: ext.id, 63 + id: name, 64 + run: func 65 + }); 66 + } else { 67 + registerWebpackModule({ ...wp, ext: ext.id, id: name }); 70 68 } 71 69 } 70 + } 72 71 73 - if (exports.styles != null) { 74 - registerStyles( 75 - exports.styles.map((style, i) => `/* ${ext.id}#${i} */ ${style}`) 76 - ); 77 - } 72 + if (exports.styles != null) { 73 + registerStyles( 74 + exports.styles.map((style, i) => `/* ${ext.id}#${i} */ ${style}`) 75 + ); 78 76 } 77 + } 78 + } 79 + 80 + async function loadExt(ext: DetectedExtension) { 81 + webTarget: { 82 + loadExtWeb(ext); 79 83 } 80 84 81 85 nodePreload: { ··· 117 121 export async function loadExtensions( 118 122 exts: DetectedExtension[] 119 123 ): Promise<ProcessedExtensions> { 120 - const config = readConfig(); 124 + const config = await readConfig(); 121 125 const items = exts 122 126 .map((ext) => { 123 127 return { ··· 205 209 logger.debug(`Loaded "${ext.id}"`); 206 210 } 207 211 208 - webPreload: { 212 + webTarget: { 209 213 for (const ext of extensions) { 210 214 moonlight.enabledExtensions.add(ext.id); 211 215 }
+66
packages/core/src/util/binary.ts
··· 1 + // https://github.com/NotNite/brc-save-editor/blob/main/src/lib/binary.ts 2 + export interface BinaryInterface { 3 + data: Uint8Array; 4 + view: DataView; 5 + length: number; 6 + position: number; 7 + } 8 + 9 + export class BinaryReader implements BinaryInterface { 10 + data: Uint8Array; 11 + view: DataView; 12 + length: number; 13 + position: number; 14 + 15 + constructor(data: Uint8Array) { 16 + this.data = data; 17 + this.view = new DataView(data.buffer); 18 + 19 + this.length = data.length; 20 + this.position = 0; 21 + } 22 + 23 + readByte() { 24 + return this._read(this.view.getInt8, 1); 25 + } 26 + 27 + readBoolean() { 28 + return this.readByte() !== 0; 29 + } 30 + 31 + readInt32() { 32 + return this._read(this.view.getInt32, 4); 33 + } 34 + 35 + readUInt32() { 36 + return this._read(this.view.getUint32, 4); 37 + } 38 + 39 + readSingle() { 40 + return this._read(this.view.getFloat32, 4); 41 + } 42 + 43 + readInt64() { 44 + return this._read(this.view.getBigInt64, 8); 45 + } 46 + 47 + readString(length: number) { 48 + const result = this.read(length); 49 + return new TextDecoder().decode(result); 50 + } 51 + 52 + read(length: number) { 53 + const data = this.data.subarray(this.position, this.position + length); 54 + this.position += length; 55 + return data; 56 + } 57 + 58 + private _read<T>( 59 + func: (position: number, littleEndian?: boolean) => T, 60 + length: number 61 + ): T { 62 + const result = func.call(this.view, this.position, true); 63 + this.position += length; 64 + return result; 65 + } 66 + }
+73 -91
packages/core/src/util/event.ts
··· 1 1 import { MoonlightEventEmitter } from "@moonlight-mod/types/core/event"; 2 2 3 - function nodeMethod< 3 + export function createEventEmitter< 4 4 EventId extends string = string, 5 5 EventData = Record<EventId, any> 6 6 >(): MoonlightEventEmitter<EventId, EventData> { 7 - const EventEmitter = require("events"); 8 - const eventEmitter = new EventEmitter(); 9 - const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 7 + webTarget: { 8 + const eventEmitter = new EventTarget(); 9 + const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 10 10 11 - return { 12 - dispatchEvent: <Id extends keyof EventData>( 13 - id: Id, 14 - data: EventData[Id] 15 - ) => { 16 - eventEmitter.emit(id as string, data); 17 - }, 18 - 19 - addEventListener: <Id extends keyof EventData>( 20 - id: Id, 21 - cb: (data: EventData[Id]) => void 22 - ) => { 23 - const untyped = cb as (data: EventData) => void; 24 - if (listeners.has(untyped)) return; 25 - 26 - function listener(e: Event) { 27 - const event = e as CustomEvent<string>; 28 - cb(event as EventData[Id]); 29 - } 30 - 31 - listeners.set(untyped, listener); 32 - eventEmitter.on(id as string, listener); 33 - }, 34 - 35 - removeEventListener: <Id extends keyof EventData>( 36 - id: Id, 37 - cb: (data: EventData[Id]) => void 38 - ) => { 39 - const untyped = cb as (data: EventData) => void; 40 - const listener = listeners.get(untyped); 41 - if (listener == null) return; 42 - listeners.delete(untyped); 43 - eventEmitter.off(id as string, listener); 44 - } 45 - }; 46 - } 11 + return { 12 + dispatchEvent: <Id extends keyof EventData>( 13 + id: Id, 14 + data: EventData[Id] 15 + ) => { 16 + eventEmitter.dispatchEvent( 17 + new CustomEvent(id as string, { detail: data }) 18 + ); 19 + }, 47 20 48 - function webMethod< 49 - EventId extends string = string, 50 - EventData = Record<EventId, any> 51 - >(): MoonlightEventEmitter<EventId, EventData> { 52 - const eventEmitter = new EventTarget(); 53 - const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 21 + addEventListener: <Id extends keyof EventData>( 22 + id: Id, 23 + cb: (data: EventData[Id]) => void 24 + ) => { 25 + const untyped = cb as (data: EventData) => void; 26 + if (listeners.has(untyped)) return; 54 27 55 - return { 56 - dispatchEvent: <Id extends keyof EventData>( 57 - id: Id, 58 - data: EventData[Id] 59 - ) => { 60 - eventEmitter.dispatchEvent( 61 - new CustomEvent(id as string, { detail: data }) 62 - ); 63 - }, 28 + function listener(e: Event) { 29 + const event = e as CustomEvent<string>; 30 + cb(event.detail as EventData[Id]); 31 + } 64 32 65 - addEventListener: <Id extends keyof EventData>( 66 - id: Id, 67 - cb: (data: EventData[Id]) => void 68 - ) => { 69 - const untyped = cb as (data: EventData) => void; 70 - if (listeners.has(untyped)) return; 33 + listeners.set(untyped, listener); 34 + eventEmitter.addEventListener(id as string, listener); 35 + }, 71 36 72 - function listener(e: Event) { 73 - const event = e as CustomEvent<string>; 74 - cb(event.detail as EventData[Id]); 37 + removeEventListener: <Id extends keyof EventData>( 38 + id: Id, 39 + cb: (data: EventData[Id]) => void 40 + ) => { 41 + const untyped = cb as (data: EventData) => void; 42 + const listener = listeners.get(untyped); 43 + if (listener == null) return; 44 + listeners.delete(untyped); 45 + eventEmitter.removeEventListener(id as string, listener); 75 46 } 47 + }; 48 + } 76 49 77 - listeners.set(untyped, listener); 78 - eventEmitter.addEventListener(id as string, listener); 79 - }, 50 + nodeTarget: { 51 + const EventEmitter = require("events"); 52 + const eventEmitter = new EventEmitter(); 53 + const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 80 54 81 - removeEventListener: <Id extends keyof EventData>( 82 - id: Id, 83 - cb: (data: EventData[Id]) => void 84 - ) => { 85 - const untyped = cb as (data: EventData) => void; 86 - const listener = listeners.get(untyped); 87 - if (listener == null) return; 88 - listeners.delete(untyped); 89 - eventEmitter.removeEventListener(id as string, listener); 90 - } 91 - }; 92 - } 55 + return { 56 + dispatchEvent: <Id extends keyof EventData>( 57 + id: Id, 58 + data: EventData[Id] 59 + ) => { 60 + eventEmitter.emit(id as string, data); 61 + }, 62 + 63 + addEventListener: <Id extends keyof EventData>( 64 + id: Id, 65 + cb: (data: EventData[Id]) => void 66 + ) => { 67 + const untyped = cb as (data: EventData) => void; 68 + if (listeners.has(untyped)) return; 93 69 94 - export function createEventEmitter< 95 - EventId extends string = string, 96 - EventData = Record<EventId, any> 97 - >(): MoonlightEventEmitter<EventId, EventData> { 98 - webPreload: { 99 - return webMethod(); 100 - } 70 + function listener(e: Event) { 71 + const event = e as CustomEvent<string>; 72 + cb(event as EventData[Id]); 73 + } 101 74 102 - nodePreload: { 103 - return nodeMethod(); 104 - } 75 + listeners.set(untyped, listener); 76 + eventEmitter.on(id as string, listener); 77 + }, 105 78 106 - injector: { 107 - return nodeMethod(); 79 + removeEventListener: <Id extends keyof EventData>( 80 + id: Id, 81 + cb: (data: EventData[Id]) => void 82 + ) => { 83 + const untyped = cb as (data: EventData) => void; 84 + const listener = listeners.get(untyped); 85 + if (listener == null) return; 86 + listeners.delete(untyped); 87 + eventEmitter.off(id as string, listener); 88 + } 89 + }; 108 90 } 109 91 110 92 throw new Error("Called createEventEmitter() in an impossible environment");
+12 -10
packages/core/src/util/logger.ts
··· 1 1 /* eslint-disable no-console */ 2 2 import { LogLevel } from "@moonlight-mod/types/logger"; 3 - import { readConfig } from "../config"; 3 + import { Config } from "@moonlight-mod/types"; 4 4 5 5 const colors = { 6 6 [LogLevel.SILLY]: "#EDD3E9", ··· 11 11 [LogLevel.ERROR]: "#FF0000" 12 12 }; 13 13 14 - const config = readConfig(); 15 14 let maxLevel = LogLevel.INFO; 16 - if (config.loggerLevel != null) { 17 - const enumValue = 18 - LogLevel[config.loggerLevel.toUpperCase() as keyof typeof LogLevel]; 19 - if (enumValue != null) { 20 - maxLevel = enumValue; 21 - } 22 - } 23 15 24 16 export default class Logger { 25 17 private name: string; ··· 57 49 const logLevel = LogLevel[level].toUpperCase(); 58 50 if (maxLevel > level) return; 59 51 60 - if (MOONLIGHT_WEB_PRELOAD) { 52 + if (MOONLIGHT_WEB_PRELOAD || MOONLIGHT_BROWSER) { 61 53 args = [ 62 54 `%c[${logLevel}]`, 63 55 `background-color: ${colors[level]}; color: #FFFFFF;`, ··· 92 84 } 93 85 } 94 86 } 87 + 88 + export function initLogger(config: Config) { 89 + if (config.loggerLevel != null) { 90 + const enumValue = 91 + LogLevel[config.loggerLevel.toUpperCase() as keyof typeof LogLevel]; 92 + if (enumValue != null) { 93 + maxLevel = enumValue; 94 + } 95 + } 96 + }
+4 -3
packages/injector/src/index.ts
··· 8 8 import { constants } from "@moonlight-mod/types"; 9 9 import { readConfig } from "@moonlight-mod/core/config"; 10 10 import { getExtensions } from "@moonlight-mod/core/extension"; 11 - import Logger from "@moonlight-mod/core/util/logger"; 11 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 12 12 import { 13 13 loadExtensions, 14 14 loadProcessedExtensions ··· 161 161 export async function inject(asarPath: string) { 162 162 isMoonlightDesktop = asarPath === "moonlightDesktop"; 163 163 try { 164 - const config = readConfig(); 165 - const extensions = getExtensions(); 164 + const config = await readConfig(); 165 + initLogger(config); 166 + const extensions = await getExtensions(); 166 167 167 168 // Duplicated in node-preload... oops 168 169 // eslint-disable-next-line no-inner-declarations
+8 -7
packages/node-preload/src/index.ts
··· 6 6 import { constants } from "@moonlight-mod/types"; 7 7 import { getExtensions } from "@moonlight-mod/core/extension"; 8 8 import { getExtensionsPath } from "@moonlight-mod/core/util/data"; 9 - import Logger from "@moonlight-mod/core/util/logger"; 9 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 10 10 import { 11 11 loadExtensions, 12 12 loadProcessedExtensions 13 13 } from "@moonlight-mod/core/extension/loader"; 14 14 15 15 async function injectGlobals() { 16 - const config = readConfig(); 17 - const extensions = getExtensions(); 18 - const processed = await loadExtensions(extensions); 16 + const config = await readConfig(); 17 + initLogger(config); 18 + const extensions = await getExtensions(); 19 + const processedExtensions = await loadExtensions(extensions); 19 20 20 21 function getConfig(ext: string) { 21 22 const val = config.extensions[ext]; ··· 25 26 26 27 global.moonlightNode = { 27 28 config, 28 - extensions: getExtensions(), 29 - processedExtensions: processed, 29 + extensions, 30 + processedExtensions, 30 31 nativesCache: {}, 31 32 getConfig, 32 33 getConfigOption: <T>(ext: string, name: string) => { ··· 48 49 writeConfig 49 50 }; 50 51 51 - await loadProcessedExtensions(processed); 52 + await loadProcessedExtensions(processedExtensions); 52 53 contextBridge.exposeInMainWorld("moonlightNode", moonlightNode); 53 54 54 55 const extCors = moonlightNode.processedExtensions.extensions
+17
packages/types/src/globals.ts
··· 38 38 writeConfig: (config: Config) => void; 39 39 }; 40 40 41 + export type MoonlightBrowserFS = { 42 + readFile: (path: string) => Promise<Uint8Array>; 43 + writeFile: (path: string, data: Uint8Array) => Promise<void>; 44 + unlink: (path: string) => Promise<void>; 45 + 46 + readdir: (path: string) => Promise<string[]>; 47 + mkdir: (path: string) => Promise<void>; 48 + rmdir: (path: string) => Promise<void>; 49 + 50 + exists: (path: string) => Promise<boolean>; 51 + isFile: (path: string) => Promise<boolean>; 52 + 53 + join: (...parts: string[]) => string; 54 + dirname: (path: string) => string; 55 + parts: (path: string) => string[]; 56 + }; 57 + 41 58 export type MoonlightWeb = { 42 59 unpatched: Set<IdentifiedPatch>; 43 60 pendingModules: Set<IdentifiedWebpackModule>;
+6
packages/types/src/index.ts
··· 5 5 /* eslint-disable no-var */ 6 6 7 7 import { 8 + MoonlightBrowserFS, 8 9 MoonlightEnv, 9 10 MoonlightHost, 10 11 MoonlightNode, ··· 28 29 const MOONLIGHT_INJECTOR: boolean; 29 30 const MOONLIGHT_NODE_PRELOAD: boolean; 30 31 const MOONLIGHT_WEB_PRELOAD: boolean; 32 + const MOONLIGHT_BROWSER: boolean; 31 33 32 34 var moonlightHost: MoonlightHost; 33 35 var moonlightNode: MoonlightNode; 34 36 var moonlight: MoonlightWeb; 37 + 38 + var _moonlightBrowserInit: () => Promise<void>; 39 + var _moonlightBrowserLoad: () => Promise<void>; 40 + var _moonlightBrowserFS: MoonlightBrowserFS | undefined; 35 41 }
+1
packages/web-preload/package.json
··· 1 1 { 2 2 "name": "@moonlight-mod/web-preload", 3 3 "private": true, 4 + "main": "src/index.ts", 4 5 "dependencies": { 5 6 "@moonlight-mod/core": "workspace:*", 6 7 "@moonlight-mod/lunast": "^1.0.0",
+10 -3
packages/web-preload/src/index.ts
··· 7 7 } from "@moonlight-mod/core/patch"; 8 8 import { constants } from "@moonlight-mod/types"; 9 9 import { installStyles } from "@moonlight-mod/core/styles"; 10 - import Logger from "@moonlight-mod/core/util/logger"; 10 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 11 11 import LunAST from "@moonlight-mod/lunast"; 12 12 import Moonmap from "@moonlight-mod/moonmap"; 13 13 import loadMappings from "@moonlight-mod/mappings"; 14 14 import { createEventEmitter } from "@moonlight-mod/core/util/event"; 15 15 import { EventPayloads, EventType } from "@moonlight-mod/types/core/event"; 16 16 17 - (async () => { 17 + async function load() { 18 + initLogger(moonlightNode.config); 18 19 const logger = new Logger("web-preload"); 19 20 20 21 window.moonlight = { ··· 50 51 window.addEventListener("DOMContentLoaded", () => { 51 52 installStyles(); 52 53 }); 53 - })(); 54 + } 55 + 56 + if (MOONLIGHT_ENV === "web-preload") { 57 + load(); 58 + } else { 59 + window._moonlightBrowserLoad = load; 60 + }
+166 -20
pnpm-lock.yaml
··· 42 42 specifier: ^5.3.2 43 43 version: 5.3.2 44 44 45 + packages/browser: 46 + dependencies: 47 + '@moonlight-mod/core': 48 + specifier: workspace:* 49 + version: link:../core 50 + '@moonlight-mod/types': 51 + specifier: workspace:* 52 + version: link:../types 53 + '@moonlight-mod/web-preload': 54 + specifier: workspace:* 55 + version: link:../web-preload 56 + '@zenfs/core': 57 + specifier: ^1.0.2 58 + version: 1.0.2 59 + '@zenfs/dom': 60 + specifier: ^0.2.16 61 + version: 0.2.16(@zenfs/core@1.0.2) 62 + 45 63 packages/core: 46 64 dependencies: 47 65 '@moonlight-mod/types': ··· 50 68 51 69 packages/core-extensions: 52 70 dependencies: 53 - '@electron/asar': 54 - specifier: ^3.2.5 55 - version: 3.2.5 71 + '@moonlight-mod/core': 72 + specifier: workspace:* 73 + version: link:../core 56 74 '@moonlight-mod/types': 57 75 specifier: workspace:* 58 76 version: link:../types ··· 119 137 '@aashutoshrathi/word-wrap@1.2.6': 120 138 resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} 121 139 engines: {node: '>=0.10.0'} 122 - 123 - '@electron/asar@3.2.5': 124 - resolution: {integrity: sha512-Ypahc2ElTj9YOrFvUHuoXv5Z/V1nPA5enlhmQapc578m/HZBHKTbqhoL5JZQjje2+/6Ti5AHh7Gj1/haeJa63Q==} 125 - engines: {node: '>=10.12.0'} 126 - hasBin: true 127 140 128 141 '@esbuild/android-arm64@0.19.3': 129 142 resolution: {integrity: sha512-w+Akc0vv5leog550kjJV9Ru+MXMR2VuMrui3C61mnysim0gkFCPOUTAfzTP0qX+HpN9Syu3YA3p1hf3EPqObRw==} ··· 334 347 '@types/node@18.17.17': 335 348 resolution: {integrity: sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==} 336 349 350 + '@types/node@20.16.10': 351 + resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==} 352 + 337 353 '@types/prop-types@15.7.13': 338 354 resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} 339 355 340 356 '@types/react@18.3.10': 341 357 resolution: {integrity: sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==} 358 + 359 + '@types/readable-stream@4.0.15': 360 + resolution: {integrity: sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==} 342 361 343 362 '@types/semver@7.5.6': 344 363 resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} ··· 404 423 '@ungap/structured-clone@1.2.0': 405 424 resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} 406 425 426 + '@zenfs/core@1.0.2': 427 + resolution: {integrity: sha512-LMTD4ntn6Ag1y+IeOSVykDDvYC12dsGFtsX8M/54OQrLs7v+YnX4bpo0o2osbm8XFmU2MTNMX/G3PLsvzgWzrg==} 428 + engines: {node: '>= 16'} 429 + hasBin: true 430 + 431 + '@zenfs/dom@0.2.16': 432 + resolution: {integrity: sha512-6Ev+ol9hZIgQECNZR+xxjQ/a99EhhrWeiQttm/+U7YJK3HdTjiKfU39DsfGeH64vSqhpa5Vj+LWRx75SHkjw0Q==} 433 + engines: {node: '>= 18'} 434 + peerDependencies: 435 + '@zenfs/core': ^1.0.0 436 + 437 + abort-controller@3.0.0: 438 + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 439 + engines: {node: '>=6.5'} 440 + 407 441 acorn-jsx@5.3.2: 408 442 resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 409 443 peerDependencies: ··· 468 502 balanced-match@1.0.2: 469 503 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 470 504 505 + base64-js@1.5.1: 506 + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 507 + 471 508 big-integer@1.6.52: 472 509 resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} 473 510 engines: {node: '>=0.6'} ··· 479 516 brace-expansion@1.1.11: 480 517 resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 481 518 519 + brace-expansion@2.0.1: 520 + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 521 + 482 522 braces@3.0.2: 483 523 resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 484 524 engines: {node: '>=8'} 485 525 526 + buffer@6.0.3: 527 + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} 528 + 486 529 bundle-name@3.0.0: 487 530 resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} 488 531 engines: {node: '>=12'} ··· 504 547 505 548 color-name@1.1.4: 506 549 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 507 - 508 - commander@5.1.0: 509 - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} 510 - engines: {node: '>= 6'} 511 550 512 551 concat-map@0.0.1: 513 552 resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} ··· 658 697 resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 659 698 engines: {node: '>=0.10.0'} 660 699 700 + event-target-shim@5.0.1: 701 + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} 702 + engines: {node: '>=6'} 703 + 704 + eventemitter3@5.0.1: 705 + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} 706 + 707 + events@3.3.0: 708 + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 709 + engines: {node: '>=0.8.x'} 710 + 661 711 execa@5.1.1: 662 712 resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} 663 713 engines: {node: '>=10'} ··· 800 850 engines: {node: '>=14'} 801 851 hasBin: true 802 852 853 + ieee754@1.2.1: 854 + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 855 + 803 856 ignore@5.3.0: 804 857 resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} 805 858 engines: {node: '>= 4'} ··· 1017 1070 minimatch@3.1.2: 1018 1071 resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1019 1072 1073 + minimatch@9.0.5: 1074 + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 1075 + engines: {node: '>=16 || 14 >=14.17'} 1076 + 1020 1077 ms@2.1.2: 1021 1078 resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 1022 1079 ··· 1135 1192 engines: {node: '>=14'} 1136 1193 hasBin: true 1137 1194 1195 + process@0.11.10: 1196 + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} 1197 + engines: {node: '>= 0.6.0'} 1198 + 1138 1199 prop-types@15.8.1: 1139 1200 resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 1140 1201 ··· 1147 1208 1148 1209 react-is@16.13.1: 1149 1210 resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 1211 + 1212 + readable-stream@4.5.2: 1213 + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} 1214 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 1150 1215 1151 1216 reflect.getprototypeof@1.0.4: 1152 1217 resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} ··· 1184 1249 resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} 1185 1250 engines: {node: '>=0.4'} 1186 1251 1252 + safe-buffer@5.1.2: 1253 + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 1254 + 1255 + safe-buffer@5.2.1: 1256 + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1257 + 1187 1258 safe-regex-test@1.0.0: 1188 1259 resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} 1189 1260 ··· 1237 1308 1238 1309 string.prototype.trimstart@1.0.7: 1239 1310 resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} 1311 + 1312 + string_decoder@1.3.0: 1313 + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 1240 1314 1241 1315 strip-ansi@6.0.1: 1242 1316 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} ··· 1317 1391 unbox-primitive@1.0.2: 1318 1392 resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} 1319 1393 1394 + undici-types@6.19.8: 1395 + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 1396 + 1320 1397 untildify@4.0.0: 1321 1398 resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} 1322 1399 engines: {node: '>=8'} 1323 1400 1324 1401 uri-js@4.4.1: 1325 1402 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1403 + 1404 + utilium@0.7.1: 1405 + resolution: {integrity: sha512-2ocvTkI7U8LERmwxL0LhFUvEfN66UqcjF6tMiURvUwSyU7U1QC9gST+3iSUSiGccFfnP3f2EXwHNXOnOzx+lAg==} 1326 1406 1327 1407 which-boxed-primitive@1.0.2: 1328 1408 resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} ··· 1357 1437 1358 1438 '@aashutoshrathi/word-wrap@1.2.6': {} 1359 1439 1360 - '@electron/asar@3.2.5': 1361 - dependencies: 1362 - commander: 5.1.0 1363 - glob: 7.2.3 1364 - minimatch: 3.1.2 1365 - 1366 1440 '@esbuild/android-arm64@0.19.3': 1367 1441 optional: true 1368 1442 ··· 1518 1592 1519 1593 '@types/node@18.17.17': {} 1520 1594 1595 + '@types/node@20.16.10': 1596 + dependencies: 1597 + undici-types: 6.19.8 1598 + 1521 1599 '@types/prop-types@15.7.13': {} 1522 1600 1523 1601 '@types/react@18.3.10': ··· 1525 1603 '@types/prop-types': 15.7.13 1526 1604 csstype: 3.1.3 1527 1605 1606 + '@types/readable-stream@4.0.15': 1607 + dependencies: 1608 + '@types/node': 20.16.10 1609 + safe-buffer: 5.1.2 1610 + 1528 1611 '@types/semver@7.5.6': {} 1529 1612 1530 1613 '@typescript-eslint/eslint-plugin@6.13.2(@typescript-eslint/parser@6.13.2(eslint@8.55.0)(typescript@5.3.2))(eslint@8.55.0)(typescript@5.3.2)': ··· 1614 1697 1615 1698 '@ungap/structured-clone@1.2.0': {} 1616 1699 1700 + '@zenfs/core@1.0.2': 1701 + dependencies: 1702 + '@types/node': 20.16.10 1703 + '@types/readable-stream': 4.0.15 1704 + buffer: 6.0.3 1705 + eventemitter3: 5.0.1 1706 + minimatch: 9.0.5 1707 + readable-stream: 4.5.2 1708 + utilium: 0.7.1 1709 + 1710 + '@zenfs/dom@0.2.16(@zenfs/core@1.0.2)': 1711 + dependencies: 1712 + '@zenfs/core': 1.0.2 1713 + 1714 + abort-controller@3.0.0: 1715 + dependencies: 1716 + event-target-shim: 5.0.1 1717 + 1617 1718 acorn-jsx@5.3.2(acorn@8.12.1): 1618 1719 dependencies: 1619 1720 acorn: 8.12.1 ··· 1692 1793 1693 1794 balanced-match@1.0.2: {} 1694 1795 1796 + base64-js@1.5.1: {} 1797 + 1695 1798 big-integer@1.6.52: {} 1696 1799 1697 1800 bplist-parser@0.2.0: ··· 1703 1806 balanced-match: 1.0.2 1704 1807 concat-map: 0.0.1 1705 1808 1809 + brace-expansion@2.0.1: 1810 + dependencies: 1811 + balanced-match: 1.0.2 1812 + 1706 1813 braces@3.0.2: 1707 1814 dependencies: 1708 1815 fill-range: 7.0.1 1816 + 1817 + buffer@6.0.3: 1818 + dependencies: 1819 + base64-js: 1.5.1 1820 + ieee754: 1.2.1 1709 1821 1710 1822 bundle-name@3.0.0: 1711 1823 dependencies: ··· 1729 1841 color-name: 1.1.4 1730 1842 1731 1843 color-name@1.1.4: {} 1732 - 1733 - commander@5.1.0: {} 1734 1844 1735 1845 concat-map@0.0.1: {} 1736 1846 ··· 1998 2108 1999 2109 esutils@2.0.3: {} 2000 2110 2111 + event-target-shim@5.0.1: {} 2112 + 2113 + eventemitter3@5.0.1: {} 2114 + 2115 + events@3.3.0: {} 2116 + 2001 2117 execa@5.1.1: 2002 2118 dependencies: 2003 2119 cross-spawn: 7.0.3 ··· 2159 2275 human-signals@4.3.1: {} 2160 2276 2161 2277 husky@8.0.3: {} 2278 + 2279 + ieee754@1.2.1: {} 2162 2280 2163 2281 ignore@5.3.0: {} 2164 2282 ··· 2360 2478 dependencies: 2361 2479 brace-expansion: 1.1.11 2362 2480 2481 + minimatch@9.0.5: 2482 + dependencies: 2483 + brace-expansion: 2.0.1 2484 + 2363 2485 ms@2.1.2: {} 2364 2486 2365 2487 natural-compare@1.4.0: {} ··· 2472 2594 2473 2595 prettier@3.1.0: {} 2474 2596 2597 + process@0.11.10: {} 2598 + 2475 2599 prop-types@15.8.1: 2476 2600 dependencies: 2477 2601 loose-envify: 1.4.0 ··· 2483 2607 queue-microtask@1.2.3: {} 2484 2608 2485 2609 react-is@16.13.1: {} 2610 + 2611 + readable-stream@4.5.2: 2612 + dependencies: 2613 + abort-controller: 3.0.0 2614 + buffer: 6.0.3 2615 + events: 3.3.0 2616 + process: 0.11.10 2617 + string_decoder: 1.3.0 2486 2618 2487 2619 reflect.getprototypeof@1.0.4: 2488 2620 dependencies: ··· 2528 2660 has-symbols: 1.0.3 2529 2661 isarray: 2.0.5 2530 2662 2663 + safe-buffer@5.1.2: {} 2664 + 2665 + safe-buffer@5.2.1: {} 2666 + 2531 2667 safe-regex-test@1.0.0: 2532 2668 dependencies: 2533 2669 call-bind: 1.0.5 ··· 2603 2739 define-properties: 1.2.1 2604 2740 es-abstract: 1.22.3 2605 2741 2742 + string_decoder@1.3.0: 2743 + dependencies: 2744 + safe-buffer: 5.2.1 2745 + 2606 2746 strip-ansi@6.0.1: 2607 2747 dependencies: 2608 2748 ansi-regex: 5.0.1 ··· 2680 2820 has-symbols: 1.0.3 2681 2821 which-boxed-primitive: 1.0.2 2682 2822 2823 + undici-types@6.19.8: {} 2824 + 2683 2825 untildify@4.0.0: {} 2684 2826 2685 2827 uri-js@4.4.1: 2686 2828 dependencies: 2687 2829 punycode: 2.3.1 2830 + 2831 + utilium@0.7.1: 2832 + dependencies: 2833 + eventemitter3: 5.0.1 2688 2834 2689 2835 which-boxed-primitive@1.0.2: 2690 2836 dependencies: