this repo has no description

Initial commit

Co-authored-by: Cynthia Foxwell <gamers@riseup.net>
Co-authored-by: adryd <me@adryd.com>

+5169
+2
.gitignore
··· 1 + node_modules/ 2 + dist/
+6
.prettierrc
··· 1 + { 2 + "printWidth": 80, 3 + "trailingComma": "none", 4 + "tabWidth": 2, 5 + "singleQuote": false 6 + }
+17
README.md
··· 1 + <h3 align="center"> 2 + <img src="./img/wordmark.png" alt="moonlight" /> 3 + 4 + <a href="https://discord.gg/FdZBTFCP6F">Discord server</a> 5 + \- <a href="https://github.com/moonlight-mod/moonlight">GitHub</a> 6 + \- <a href="https://moonlight-mod.github.io/">Docs</a> 7 + 8 + <hr /> 9 + </h3> 10 + 11 + **moonlight** is yet another Discord client mod, focused on providing a decent user and developer experience. 12 + 13 + moonlight is heavily inspired by hh3 (a private client mod) and the projects before it that it is inspired by, namely EndPwn. All core code is original or used with permission from their respective authors where not copyleft. 14 + 15 + **_This is an experimental passion project._** moonlight was not created out of malicious intent nor intended to seriously compete with other mods. Anything and everything is subject to change. 16 + 17 + moonlight is licensed under the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) (`AGPL-3.0-or-later`). See [the documentation](https://moonlight-mod.github.io/) for more information.
+137
build.mjs
··· 1 + import * as esbuild from "esbuild"; 2 + import copyStaticFiles from "esbuild-copy-static-files"; 3 + 4 + import path from "path"; 5 + import fs from "fs"; 6 + 7 + const config = { 8 + injector: "packages/injector/src/index.ts", 9 + "node-preload": "packages/node-preload/src/index.ts", 10 + "web-preload": "packages/web-preload/src/index.ts" 11 + }; 12 + 13 + const prod = process.env.NODE_ENV === "production"; 14 + const watch = process.argv.includes("--watch"); 15 + 16 + const external = [ 17 + "electron", 18 + "fs", 19 + "path", 20 + "module", 21 + "events", 22 + "original-fs", // wtf asar? 23 + 24 + // Silence an esbuild warning 25 + "./node-preload.js" 26 + ]; 27 + 28 + async function build(name, entry) { 29 + const outfile = path.join("./dist", name + ".js"); 30 + 31 + const dropLabels = []; 32 + if (name !== "injector") dropLabels.push("injector"); 33 + if (name !== "node-preload") dropLabels.push("nodePreload"); 34 + if (name !== "web-preload") dropLabels.push("webPreload"); 35 + 36 + const define = { 37 + MOONLIGHT_ENV: `"${name}"`, 38 + MOONLIGHT_PROD: prod.toString() 39 + }; 40 + 41 + for (const iterName of Object.keys(config)) { 42 + const snake = iterName.replace(/-/g, "_").toUpperCase(); 43 + define[`MOONLIGHT_${snake}`] = (name === iterName).toString(); 44 + } 45 + 46 + const nodeDependencies = ["glob"]; 47 + const ignoredExternal = name === "web-preload" ? nodeDependencies : []; 48 + 49 + const esbuildConfig = { 50 + entryPoints: [entry], 51 + outfile, 52 + 53 + format: "cjs", 54 + platform: name === "web-preload" ? "browser" : "node", 55 + 56 + treeShaking: true, 57 + bundle: true, 58 + sourcemap: prod ? false : "inline", 59 + 60 + external: [...ignoredExternal, ...external], 61 + 62 + define, 63 + dropLabels 64 + }; 65 + 66 + if (watch) { 67 + const ctx = await esbuild.context(esbuildConfig); 68 + await ctx.watch(); 69 + } else { 70 + await esbuild.build(esbuildConfig); 71 + } 72 + } 73 + 74 + async function buildExt(ext, side, copyManifest, fileExt) { 75 + const outDir = path.join("./dist", "core-extensions", ext); 76 + if (!fs.existsSync(outDir)) { 77 + fs.mkdirSync(outDir, { recursive: true }); 78 + } 79 + 80 + const entryPoint = `packages/core-extensions/src/${ext}/${side}.${fileExt}`; 81 + 82 + const esbuildConfig = { 83 + entryPoints: [entryPoint], 84 + outfile: path.join(outDir, side + ".js"), 85 + 86 + format: "cjs", 87 + platform: "node", 88 + 89 + treeShaking: true, 90 + bundle: true, 91 + sourcemap: prod ? false : "inline", 92 + 93 + external, 94 + 95 + plugins: copyManifest 96 + ? [ 97 + copyStaticFiles({ 98 + src: `./packages/core-extensions/src/${ext}/manifest.json`, 99 + dest: `./dist/core-extensions/${ext}/manifest.json` 100 + }) 101 + ] 102 + : [] 103 + }; 104 + 105 + if (watch) { 106 + const ctx = await esbuild.context(esbuildConfig); 107 + await ctx.watch(); 108 + } else { 109 + await esbuild.build(esbuildConfig); 110 + } 111 + } 112 + 113 + const promises = []; 114 + 115 + for (const [name, entry] of Object.entries(config)) { 116 + promises.push(build(name, entry)); 117 + } 118 + 119 + const coreExtensions = fs.readdirSync("./packages/core-extensions/src"); 120 + for (const ext of coreExtensions) { 121 + let copiedManifest = false; 122 + 123 + for (const fileExt of ["ts", "tsx"]) { 124 + for (const type of ["index", "node", "host"]) { 125 + if ( 126 + fs.existsSync( 127 + `./packages/core-extensions/src/${ext}/${type}.${fileExt}` 128 + ) 129 + ) { 130 + promises.push(buildExt(ext, type, !copiedManifest, fileExt)); 131 + copiedManifest = true; 132 + } 133 + } 134 + } 135 + } 136 + 137 + await Promise.all(promises);
+1
env.d.ts
··· 1 + /// <reference types="./packages/types/src/index.d.ts" />
img/wordmark.png

This is a binary file and will not be displayed.

+21
package.json
··· 1 + { 2 + "name": "moonlight", 3 + "version": "1.0.0", 4 + "description": "Yet another Discord mod", 5 + "homepage": "https://github.com/moonlight-mod/moonlight#readme", 6 + "repository": { 7 + "type": "git", 8 + "url": "git+https://github.com/moonlight-mod/moonlight.git" 9 + }, 10 + "bugs": { 11 + "url": "https://github.com/moonlight-mod/moonlight/issues" 12 + }, 13 + "scripts": { 14 + "build": "node build.mjs", 15 + "dev": "node build.mjs --watch" 16 + }, 17 + "devDependencies": { 18 + "esbuild": "^0.19.3", 19 + "esbuild-copy-static-files": "^0.1.0" 20 + } 21 + }
+8
packages/core-extensions/package.json
··· 1 + { 2 + "name": "@moonlight-mod/core-extensions", 3 + "private": true, 4 + "dependencies": { 5 + "@electron/asar": "^3.2.5", 6 + "@moonlight-mod/types": "workspace:*" 7 + } 8 + }
+41
packages/core-extensions/src/common/components.ts
··· 1 + import { ExtensionWebpackModule } from "@moonlight-mod/types"; 2 + import { CommonComponents } from "@moonlight-mod/types/coreExtensions"; 3 + 4 + export const components: ExtensionWebpackModule = { 5 + dependencies: [ 6 + { ext: "spacepack", id: "spacepack" }, 7 + "MasonryList:", 8 + ".flexGutterSmall," 9 + //"ALWAYS_WHITE:", 10 + //".Messages.SWITCH_ACCOUNTS_TOAST_LOGIN_SUCCESS.format" 11 + ], 12 + run: function (module, exports, require) { 13 + const spacepack = require("spacepack_spacepack"); 14 + 15 + const Components = spacepack.findByCode("MasonryList:function")[0].exports; 16 + const MarkdownParser = spacepack.findByCode( 17 + "parseAutoModerationSystemMessage:" 18 + )[0].exports.default; 19 + const LegacyText = spacepack.findByCode(".selectable", ".colorStandard")[0] 20 + .exports.default; 21 + const Flex = spacepack.findByCode(".flex" + "GutterSmall,")[0].exports 22 + .default; 23 + const CardClasses = spacepack.findByCode("card", "cardHeader", "inModal")[0] 24 + .exports; 25 + const ControlClasses = spacepack.findByCode( 26 + "title", 27 + "titleDefault", 28 + "dividerDefault" 29 + )[0].exports; 30 + 31 + let cache: Partial<CommonComponents> = {}; 32 + module.exports = { 33 + ...Components, 34 + MarkdownParser, 35 + LegacyText, 36 + Flex, 37 + CardClasses, 38 + ControlClasses 39 + }; 40 + } 41 + };
+13
packages/core-extensions/src/common/flux.ts
··· 1 + import { ExtensionWebpackModule } from "@moonlight-mod/types"; 2 + import { CommonFlux } from "@moonlight-mod/types/coreExtensions"; 3 + 4 + const findFlux = ["useStateFromStores:function"]; 5 + 6 + export const flux: ExtensionWebpackModule = { 7 + dependencies: [{ ext: "spacepack", id: "spacepack" }, ...findFlux], 8 + run: (module, exports, require) => { 9 + const spacepack = require("spacepack_spacepack"); 10 + const Flux = spacepack.findByCode(...findFlux)[0].exports; 11 + module.exports = Flux as CommonFlux; 12 + } 13 + };
+16
packages/core-extensions/src/common/fluxDispatcher.ts
··· 1 + import { ExtensionWebpackModule } from "@moonlight-mod/types"; 2 + 3 + export const fluxDispatcher: ExtensionWebpackModule = { 4 + dependencies: [ 5 + { ext: "spacepack", id: "spacepack" }, 6 + "isDispatching", 7 + "dispatch" 8 + ], 9 + run: (module, exports, require) => { 10 + const spacepack = require("spacepack_spacepack"); 11 + module.exports = spacepack.findByExports( 12 + "isDispatching", 13 + "dispatch" 14 + )[0].exports.default; 15 + } 16 + };
+12
packages/core-extensions/src/common/http.ts
··· 1 + import { ExtensionWebpackModule } from "@moonlight-mod/types"; 2 + 3 + const findHTTP = ["get", "put", "V8APIError"]; 4 + 5 + export const http: ExtensionWebpackModule = { 6 + dependencies: [{ ext: "spacepack", id: "spacepack" }], 7 + run: (module, exports, require) => { 8 + const spacepack = require("spacepack_spacepack"); 9 + const HTTP = spacepack.findByExports(...findHTTP)[0].exports; 10 + module.exports = HTTP.ZP ?? HTTP.Z; 11 + } 12 + };
+17
packages/core-extensions/src/common/index.ts
··· 1 + import { ExtensionWebExports } from "@moonlight-mod/types"; 2 + 3 + import { react } from "./react"; 4 + import { flux } from "./flux"; 5 + import { stores } from "./stores"; 6 + import { http } from "./http"; 7 + import { components } from "./components"; 8 + import { fluxDispatcher } from "./fluxDispatcher"; 9 + 10 + export const webpackModules: ExtensionWebExports["webpackModules"] = { 11 + react, 12 + flux, 13 + stores, 14 + http, 15 + components, 16 + fluxDispatcher 17 + };
+10
packages/core-extensions/src/common/manifest.json
··· 1 + { 2 + "id": "common", 3 + "meta": { 4 + "name": "Common", 5 + "tagline": "A *lot* of common clientmodding utilities from the Discord client", 6 + "authors": ["Cynosphere", "NotNite"], 7 + "tags": ["library"] 8 + }, 9 + "dependencies": ["spacepack"] 10 + }
+15
packages/core-extensions/src/common/react.ts
··· 1 + import { ExtensionWebpackModule } from "@moonlight-mod/types"; 2 + 3 + const findReact = [ 4 + "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 5 + /\.?version(?:=|:)/, 6 + /\.?createElement(?:=|:)/ 7 + ]; 8 + 9 + export const react: ExtensionWebpackModule = { 10 + dependencies: [...findReact, { ext: "spacepack", id: "spacepack" }], 11 + run: (module, exports, require) => { 12 + const spacepack = require("spacepack_spacepack"); 13 + module.exports = spacepack.findByCode(...findReact)[0].exports; 14 + } 15 + };
+30
packages/core-extensions/src/common/stores.ts
··· 1 + import { ExtensionWebpackModule } from "@moonlight-mod/types"; 2 + 3 + export const stores: ExtensionWebpackModule = { 4 + dependencies: [{ ext: "common", id: "flux" }], 5 + run: (module, exports, require) => { 6 + const Flux = require("common_flux"); 7 + 8 + module.exports = new Proxy( 9 + {}, 10 + { 11 + get: function (target, key, receiver) { 12 + const allStores = Flux.Store.getAll(); 13 + 14 + let targetStore; 15 + for (const store of allStores) { 16 + const name = store.getName(); 17 + if (name.length == 1) continue; // filter out unnamed stores 18 + 19 + if (name == key) { 20 + targetStore = store; 21 + break; 22 + } 23 + } 24 + 25 + return targetStore; 26 + } 27 + } 28 + ); 29 + } 30 + };
+38
packages/core-extensions/src/disableSentry/host.ts
··· 1 + import { join } from "node:path"; 2 + import { Module } from "node:module"; 3 + import { BrowserWindow } from "electron"; 4 + 5 + const logger = moonlightHost.getLogger("disableSentry"); 6 + 7 + try { 8 + const hostSentryPath = require.resolve( 9 + join(moonlightHost.asarPath, "node_modules", "@sentry", "electron") 10 + ); 11 + require.cache[hostSentryPath] = new Module( 12 + hostSentryPath, 13 + require.cache[require.resolve(moonlightHost.asarPath)] 14 + ); 15 + require.cache[hostSentryPath]!.exports = { 16 + init: () => {}, 17 + captureException: () => {}, 18 + setTag: () => {}, 19 + setUser: () => {} 20 + }; 21 + logger.debug("Stubbed Sentry host side!"); 22 + } catch (err) { 23 + logger.error("Failed to stub Sentry host side:", err); 24 + } 25 + 26 + moonlightHost.events.on("window-created", (window: BrowserWindow) => { 27 + window.webContents.session.webRequest.onBeforeRequest( 28 + { 29 + urls: [ 30 + "https://*.sentry.io/*", 31 + "https://*.discord.com/error-reporting-proxy/*" 32 + ] 33 + }, 34 + function (details, callback) { 35 + callback({ cancel: true }); 36 + } 37 + ); 38 + });
+78
packages/core-extensions/src/disableSentry/index.ts
··· 1 + import { ExtensionWebExports } from "@moonlight-mod/types"; 2 + import { Patch, PatchReplaceType } from "@moonlight-mod/types"; 3 + 4 + export const patches: Patch[] = [ 5 + { 6 + find: "DSN:function", 7 + replace: { 8 + type: PatchReplaceType.Normal, 9 + match: /default:function\(\){return .}/, 10 + replacement: 'default:function(){return require("disableSentry_stub")()}' 11 + } 12 + }, 13 + { 14 + find: "window.DiscordSentry.addBreadcrumb", 15 + replace: { 16 + type: PatchReplaceType.Normal, 17 + match: /default:function\(\){return .}/, 18 + replacement: 19 + 'default:function(){return (...args)=>{moonlight.getLogger("disableSentry").debug("Sentry calling addBreadcrumb passthrough:", ...args);}}' 20 + } 21 + }, 22 + { 23 + find: "initSentry:function", 24 + replace: { 25 + type: PatchReplaceType.Normal, 26 + match: /initSentry:function\(\){return .}/, 27 + replacement: "default:function(){return ()=>{}}" 28 + } 29 + }, 30 + { 31 + find: "window.DiscordErrors=", 32 + replace: { 33 + type: PatchReplaceType.Normal, 34 + match: /uses_client_mods:\(0,.\.usesClientMods\)\(\)/, 35 + replacement: "uses_client_mods:false" 36 + } 37 + } 38 + ]; 39 + 40 + export const webpackModules: ExtensionWebExports["webpackModules"] = { 41 + stub: { 42 + run: function (module, exports, require) { 43 + const logger = moonlight.getLogger("disableSentry"); 44 + 45 + const keys = [ 46 + "setUser", 47 + "clearUser", 48 + "setTags", 49 + "setExtra", 50 + "captureException", 51 + "captureCrash", 52 + "captureMessage", 53 + "addBreadcrumb" 54 + ]; 55 + 56 + module.exports = () => 57 + new Proxy( 58 + {}, 59 + { 60 + get(target, prop, receiver) { 61 + if (prop === "profiledRootComponent") { 62 + return (arg: any) => arg; 63 + } else if (prop === "crash") { 64 + return () => { 65 + throw Error("crash"); 66 + }; 67 + } else if (keys.includes(prop.toString())) { 68 + return (...args: any[]) => 69 + logger.debug(`Sentry calling "${prop.toString()}":`, ...args); 70 + } else { 71 + return undefined; 72 + } 73 + } 74 + } 75 + ); 76 + } 77 + } 78 + };
+9
packages/core-extensions/src/disableSentry/manifest.json
··· 1 + { 2 + "id": "disableSentry", 3 + "meta": { 4 + "name": "Disable Sentry", 5 + "tagline": "Turns off Discord's error reporting systems", 6 + "authors": ["Cynosphere", "NotNite"], 7 + "tags": ["privacy"] 8 + } 9 + }
+25
packages/core-extensions/src/disableSentry/node.ts
··· 1 + import Module from "module"; 2 + import { ipcRenderer } from "electron"; 3 + import { resolve } from "path"; 4 + import { constants } from "@moonlight-mod/types"; 5 + 6 + const logger = moonlightNode.getLogger("disableSentry"); 7 + 8 + const preloadPath = ipcRenderer.sendSync(constants.ipcGetOldPreloadPath); 9 + try { 10 + const sentryPath = require.resolve( 11 + resolve(preloadPath, "..", "node_modules", "@sentry", "electron") 12 + ); 13 + require.cache[sentryPath] = new Module( 14 + sentryPath, 15 + require.cache[require.resolve(preloadPath)] 16 + ); 17 + require.cache[sentryPath]!.exports = { 18 + init: () => {}, 19 + setTag: () => {}, 20 + setUser: () => {} 21 + }; 22 + logger.debug("Stubbed Sentry node side!"); 23 + } catch (err) { 24 + logger.error("Failed to stub Sentry:", err); 25 + }
+28
packages/core-extensions/src/experiments/index.ts
··· 1 + import { Patch } from "@moonlight-mod/types"; 2 + 3 + export const patches: Patch[] = [ 4 + { 5 + find: /\.displayName="(Developer)?ExperimentStore"/, 6 + replace: { 7 + match: "window.GLOBAL_ENV.RELEASE_CHANNEL", 8 + replacement: '"staging"' 9 + } 10 + }, 11 + { 12 + find: '.displayName="DeveloperExperimentStore"', 13 + replace: [ 14 + { 15 + match: /CONNECTION_OPEN:.,OVERLAY_INITIALIZE:.,CURRENT_USER_UPDATE:./, 16 + replacement: "" 17 + }, 18 + { 19 + match: '"production"', 20 + replacement: '"development"' 21 + }, 22 + { 23 + match: /(get:function\(\){return .}}}\);).\(\);/, 24 + replacement: "$1" 25 + } 26 + ] 27 + } 28 + ];
+9
packages/core-extensions/src/experiments/manifest.json
··· 1 + { 2 + "id": "experiments", 3 + "meta": { 4 + "name": "Experiments", 5 + "tagline": "Allows you to configure Discord's internal A/B testing features", 6 + "authors": ["NotNite"], 7 + "tags": ["dangerZone"] 8 + } 9 + }
+58
packages/core-extensions/src/moonbase/index.tsx
··· 1 + import { ExtensionWebExports } from "@moonlight-mod/types"; 2 + import ui from "./ui"; 3 + import { stores } from "./stores"; 4 + import { DownloadIconSVG, TrashIconSVG } from "./types"; 5 + 6 + export const webpackModules: ExtensionWebExports["webpackModules"] = { 7 + stores: { 8 + dependencies: [ 9 + { ext: "common", id: "flux" }, 10 + { ext: "common", id: "fluxDispatcher" } 11 + ], 12 + run: (module, exports, require) => { 13 + module.exports = stores(require); 14 + } 15 + }, 16 + 17 + moonbase: { 18 + dependencies: [ 19 + { ext: "spacepack", id: "spacepack" }, 20 + { ext: "settings", id: "settings" }, 21 + { ext: "common", id: "react" }, 22 + { ext: "common", id: "components" }, 23 + { ext: "moonbase", id: "stores" }, 24 + DownloadIconSVG, 25 + TrashIconSVG 26 + ], 27 + entrypoint: true, 28 + run: (module, exports, require) => { 29 + const settings = require("settings_settings"); 30 + const React = require("common_react"); 31 + const spacepack = require("spacepack_spacepack"); 32 + const { MoonbaseSettingsStore } = 33 + require("moonbase_stores") as ReturnType< 34 + typeof import("./stores")["stores"] 35 + >; 36 + 37 + settings.addSection("Moonbase", "Moonbase", ui(require), null, -2, { 38 + stores: [MoonbaseSettingsStore], 39 + element: () => { 40 + // Require it here because lazy loading SUX 41 + const SettingsNotice = 42 + spacepack.findByCode("onSaveButtonColor")[0].exports.default; 43 + return ( 44 + <SettingsNotice 45 + submitting={MoonbaseSettingsStore.submitting} 46 + onReset={() => { 47 + MoonbaseSettingsStore.reset(); 48 + }} 49 + onSave={() => { 50 + MoonbaseSettingsStore.writeConfig(); 51 + }} 52 + /> 53 + ); 54 + } 55 + }); 56 + } 57 + } 58 + };
+9
packages/core-extensions/src/moonbase/manifest.json
··· 1 + { 2 + "id": "moonbase", 3 + "meta": { 4 + "name": "Moonbase", 5 + "tagline": "The official settings UI for moonlight", 6 + "authors": ["Cynosphere", "NotNite"] 7 + }, 8 + "dependencies": ["spacepack", "settings", "common"] 9 + }
+67
packages/core-extensions/src/moonbase/node.ts
··· 1 + import { MoonbaseNatives, RepositoryManifest } from "./types"; 2 + import asar from "@electron/asar"; 3 + import fs from "fs"; 4 + import path from "path"; 5 + import os from "os"; 6 + import { repoUrlFile } from "types/src/constants"; 7 + 8 + async function fetchRepositories(repos: string[]) { 9 + const ret: Record<string, RepositoryManifest[]> = {}; 10 + 11 + for (const repo of repos) { 12 + try { 13 + const req = await fetch(repo); 14 + const json = await req.json(); 15 + ret[repo] = json; 16 + } catch (e) { 17 + console.error(`Error fetching repository ${repo}`, e); 18 + } 19 + } 20 + 21 + return ret; 22 + } 23 + 24 + async function installExtension( 25 + manifest: RepositoryManifest, 26 + url: string, 27 + repo: string 28 + ) { 29 + const req = await fetch(url); 30 + 31 + const dir = moonlightNode.getExtensionDir(manifest.id); 32 + // remake it in case of updates 33 + if (fs.existsSync(dir)) fs.rmdirSync(dir, { recursive: true }); 34 + fs.mkdirSync(dir, { recursive: true }); 35 + 36 + // for some reason i just can't .writeFileSync() a file that ends in .asar??? 37 + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "moonlight-")); 38 + const tempFile = path.join(tempDir, "extension"); 39 + const buffer = await req.arrayBuffer(); 40 + fs.writeFileSync(tempFile, Buffer.from(buffer)); 41 + 42 + asar.extractAll(tempFile, dir); 43 + fs.writeFileSync(path.join(dir, repoUrlFile), repo); 44 + } 45 + 46 + async function deleteExtension(id: string) { 47 + const dir = moonlightNode.getExtensionDir(id); 48 + fs.rmdirSync(dir, { recursive: true }); 49 + } 50 + 51 + function getExtensionConfig(id: string, key: string): any { 52 + const config = moonlightNode.config.extensions[id]; 53 + if (typeof config === "object") { 54 + return config.config?.[key]; 55 + } 56 + 57 + return undefined; 58 + } 59 + 60 + const exports: MoonbaseNatives = { 61 + fetchRepositories, 62 + installExtension, 63 + deleteExtension, 64 + getExtensionConfig 65 + }; 66 + 67 + module.exports = exports;
+257
packages/core-extensions/src/moonbase/stores.ts
··· 1 + import WebpackRequire from "@moonlight-mod/types/discord/require"; 2 + import { 3 + Config, 4 + DetectedExtension, 5 + ExtensionLoadSource 6 + } from "@moonlight-mod/types"; 7 + import { 8 + ExtensionState, 9 + MoonbaseExtension, 10 + MoonbaseNatives, 11 + RepositoryManifest 12 + } from "./types"; 13 + 14 + export const stores = (require: typeof WebpackRequire) => { 15 + const Flux = require("common_flux"); 16 + const Dispatcher = require("common_fluxDispatcher"); 17 + const natives: MoonbaseNatives = moonlight.getNatives("moonbase"); 18 + 19 + class MoonbaseSettingsStore extends Flux.Store<any> { 20 + private origConfig: Config; 21 + private config: Config; 22 + 23 + modified: boolean; 24 + submitting: boolean; 25 + installing: boolean; 26 + 27 + extensions: { [id: string]: MoonbaseExtension }; 28 + updates: { [id: string]: { version: string; download: string } }; 29 + 30 + constructor() { 31 + super(Dispatcher); 32 + 33 + // Fucking Electron making it immutable 34 + this.origConfig = moonlightNode.config; 35 + this.config = JSON.parse(JSON.stringify(this.origConfig)); 36 + 37 + this.modified = false; 38 + this.submitting = false; 39 + this.installing = false; 40 + 41 + this.extensions = {}; 42 + this.updates = {}; 43 + for (const ext of moonlightNode.extensions) { 44 + const existingExtension = this.extensions[ext.id]; 45 + if (existingExtension != null) continue; 46 + 47 + this.extensions[ext.id] = { 48 + ...ext, 49 + state: moonlight.enabledExtensions.has(ext.id) 50 + ? ExtensionState.Enabled 51 + : ExtensionState.Disabled 52 + }; 53 + } 54 + 55 + natives.fetchRepositories(this.config.repositories).then((ret) => { 56 + for (const [repo, exts] of Object.entries(ret)) { 57 + try { 58 + for (const ext of exts) { 59 + try { 60 + const existingExtension = this.extensions[ext.id]; 61 + if (existingExtension != null) { 62 + if (this.hasUpdate(repo, ext, existingExtension)) { 63 + this.updates[ext.id] = { 64 + version: ext.version!, 65 + download: ext.download 66 + }; 67 + } 68 + continue; 69 + } 70 + 71 + this.extensions[ext.id] = { 72 + id: ext.id, 73 + manifest: ext, 74 + source: { type: ExtensionLoadSource.Normal, url: repo }, 75 + state: ExtensionState.NotDownloaded 76 + }; 77 + } catch (e) { 78 + console.error(`Error processing extension ${ext.id}`, e); 79 + } 80 + } 81 + } catch (e) { 82 + console.error(`Error processing repository ${repo}`, e); 83 + } 84 + } 85 + 86 + this.emitChange(); 87 + }); 88 + } 89 + 90 + // this logic sucks so bad lol 91 + private hasUpdate( 92 + repo: string, 93 + repoExt: RepositoryManifest, 94 + existing: MoonbaseExtension 95 + ) { 96 + return ( 97 + existing.source.type === ExtensionLoadSource.Normal && 98 + existing.source.url != null && 99 + existing.source.url === repo && 100 + repoExt.version != null && 101 + existing.manifest.version != repoExt.version 102 + ); 103 + } 104 + 105 + // Jank 106 + private isModified() { 107 + const orig = JSON.stringify(this.origConfig); 108 + const curr = JSON.stringify(this.config); 109 + return orig !== curr; 110 + } 111 + 112 + get busy() { 113 + return this.submitting || this.installing; 114 + } 115 + 116 + showNotice() { 117 + return this.modified; 118 + } 119 + 120 + getExtension(id: string) { 121 + return this.extensions[id]; 122 + } 123 + 124 + getExtensionName(id: string) { 125 + return this.extensions.hasOwnProperty(id) 126 + ? this.extensions[id].manifest.meta?.name ?? id 127 + : id; 128 + } 129 + 130 + getExtensionUpdate(id: string) { 131 + return this.updates.hasOwnProperty(id) ? this.updates[id] : null; 132 + } 133 + 134 + getExtensionEnabled(id: string) { 135 + const val = this.config.extensions[id]; 136 + if (val == null) return false; 137 + return typeof val === "boolean" ? val : val.enabled; 138 + } 139 + 140 + getExtensionConfig<T>(id: string, key: string): T | undefined { 141 + const defaultValue = 142 + this.extensions[id].manifest.settings?.[key]?.default; 143 + const cfg = this.config.extensions[id]; 144 + 145 + if (cfg == null || typeof cfg === "boolean") return defaultValue; 146 + return cfg.config?.[key] ?? defaultValue; 147 + } 148 + 149 + getExtensionConfigName(id: string, key: string) { 150 + return this.extensions[id].manifest.settings?.[key]?.displayName ?? key; 151 + } 152 + 153 + setExtensionConfig(id: string, key: string, value: any) { 154 + const oldConfig = this.config.extensions[id]; 155 + const newConfig = 156 + typeof oldConfig === "boolean" 157 + ? { 158 + enabled: oldConfig, 159 + config: { [key]: value } 160 + } 161 + : { 162 + ...oldConfig, 163 + config: { ...(oldConfig?.config ?? {}), [key]: value } 164 + }; 165 + 166 + this.config.extensions[id] = newConfig; 167 + this.modified = this.isModified(); 168 + this.emitChange(); 169 + } 170 + 171 + setExtensionEnabled(id: string, enabled: boolean) { 172 + let val = this.config.extensions[id]; 173 + 174 + if (val == null) { 175 + this.config.extensions[id] = { enabled }; 176 + this.emitChange(); 177 + return; 178 + } 179 + 180 + if (typeof val === "boolean") { 181 + val = enabled; 182 + } else { 183 + val.enabled = enabled; 184 + } 185 + 186 + this.config.extensions[id] = val; 187 + this.modified = this.isModified(); 188 + this.emitChange(); 189 + } 190 + 191 + async installExtension(id: string) { 192 + const ext = this.getExtension(id); 193 + if (!("download" in ext.manifest)) { 194 + throw new Error("Extension has no download URL"); 195 + } 196 + 197 + this.installing = true; 198 + try { 199 + const url = this.updates[id]?.download ?? ext.manifest.download; 200 + await natives.installExtension(ext.manifest, url, ext.source.url!); 201 + if (ext.state === ExtensionState.NotDownloaded) { 202 + this.extensions[id].state = ExtensionState.Disabled; 203 + } 204 + 205 + delete this.updates[id]; 206 + } catch (e) { 207 + console.error("Error installing extension:", e); 208 + } 209 + 210 + this.installing = false; 211 + this.emitChange(); 212 + } 213 + 214 + async deleteExtension(id: string) { 215 + const ext = this.getExtension(id); 216 + if (ext == null) return; 217 + 218 + this.installing = true; 219 + try { 220 + await natives.deleteExtension(ext.id); 221 + this.extensions[id].state = ExtensionState.NotDownloaded; 222 + } catch (e) { 223 + console.error("Error deleting extension:", e); 224 + } 225 + 226 + this.installing = false; 227 + this.emitChange(); 228 + } 229 + 230 + writeConfig() { 231 + this.submitting = true; 232 + 233 + try { 234 + moonlightNode.writeConfig(this.config); 235 + // I love jank cloning 236 + this.origConfig = JSON.parse(JSON.stringify(this.config)); 237 + } catch (e) { 238 + console.error("Error writing config", e); 239 + } 240 + 241 + this.submitting = false; 242 + this.modified = false; 243 + this.emitChange(); 244 + } 245 + 246 + reset() { 247 + this.submitting = false; 248 + this.modified = false; 249 + this.config = JSON.parse(JSON.stringify(this.origConfig)); 250 + this.emitChange(); 251 + } 252 + } 253 + 254 + return { 255 + MoonbaseSettingsStore: new MoonbaseSettingsStore() 256 + }; 257 + };
+36
packages/core-extensions/src/moonbase/types.ts
··· 1 + import { DetectedExtension, ExtensionManifest } from "types/src"; 2 + 3 + export const DownloadIconSVG = 4 + "M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"; 5 + export const TrashIconSVG = 6 + "M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"; 7 + 8 + export type MoonbaseNatives = { 9 + fetchRepositories( 10 + repos: string[] 11 + ): Promise<Record<string, RepositoryManifest[]>>; 12 + installExtension( 13 + manifest: RepositoryManifest, 14 + url: string, 15 + repo: string 16 + ): Promise<void>; 17 + deleteExtension(id: string): Promise<void>; 18 + getExtensionConfig(id: string, key: string): any; 19 + }; 20 + 21 + export type RepositoryManifest = ExtensionManifest & { 22 + download: string; 23 + }; 24 + 25 + export enum ExtensionState { 26 + NotDownloaded, 27 + Disabled, 28 + Enabled 29 + } 30 + 31 + export type MoonbaseExtension = { 32 + id: string; 33 + manifest: ExtensionManifest | RepositoryManifest; 34 + source: DetectedExtension["source"]; 35 + state: ExtensionState; 36 + };
+228
packages/core-extensions/src/moonbase/ui/index.tsx
··· 1 + import WebpackRequire from "@moonlight-mod/types/discord/require"; 2 + import { DownloadIconSVG, ExtensionState, TrashIconSVG } from "../types"; 3 + import { ExtensionLoadSource } from "types/src"; 4 + import info from "./info"; 5 + import settings from "./settings"; 6 + 7 + export enum ExtensionPage { 8 + Info, 9 + Description, 10 + Settings 11 + } 12 + 13 + export default (require: typeof WebpackRequire) => { 14 + const React = require("common_react"); 15 + const spacepack = require("spacepack_spacepack"); 16 + const CommonComponents = require("common_components"); 17 + const Flux = require("common_flux"); 18 + 19 + const { ExtensionInfo } = info(require); 20 + const { Settings } = settings(require); 21 + const { MoonbaseSettingsStore } = require("moonbase_stores") as ReturnType< 22 + typeof import("../stores")["stores"] 23 + >; 24 + 25 + const UserProfileClasses = spacepack.findByCode( 26 + "tabBarContainer", 27 + "topSection" 28 + )[0].exports; 29 + 30 + const DownloadIcon = spacepack.findByCode(DownloadIconSVG)[0].exports.default; 31 + const TrashIcon = spacepack.findByCode(TrashIconSVG)[0].exports.default; 32 + 33 + function ExtensionCard({ id }: { id: string }) { 34 + const [tab, setTab] = React.useState(ExtensionPage.Info); 35 + const { ext, enabled, busy, update } = Flux.useStateFromStores( 36 + [MoonbaseSettingsStore], 37 + () => { 38 + return { 39 + ext: MoonbaseSettingsStore.getExtension(id), 40 + enabled: MoonbaseSettingsStore.getExtensionEnabled(id), 41 + busy: MoonbaseSettingsStore.busy, 42 + update: MoonbaseSettingsStore.getExtensionUpdate(id) 43 + }; 44 + } 45 + ); 46 + 47 + // Why it work like that :sob: 48 + if (ext == null) return <></>; 49 + 50 + const { 51 + Card, 52 + CardClasses, 53 + Flex, 54 + Text, 55 + MarkdownParser, 56 + Switch, 57 + TabBar, 58 + Button 59 + } = CommonComponents; 60 + 61 + const tagline = ext.manifest?.meta?.tagline; 62 + const settings = ext.manifest?.settings; 63 + const description = ext.manifest?.meta?.description; 64 + 65 + return ( 66 + <Card editable={true} className={CardClasses.card}> 67 + <div className={CardClasses.cardHeader}> 68 + <Flex direction={Flex.Direction.VERTICAL}> 69 + <Flex direction={Flex.Direction.HORIZONTAL}> 70 + <Text variant="text-md/semibold"> 71 + {ext.manifest?.meta?.name ?? ext.id} 72 + </Text> 73 + </Flex> 74 + 75 + {tagline != null && ( 76 + <Text variant="text-sm/normal"> 77 + {MarkdownParser.parse(tagline)} 78 + </Text> 79 + )} 80 + </Flex> 81 + 82 + <Flex 83 + direction={Flex.Direction.HORIZONTAL} 84 + align={Flex.Align.END} 85 + justify={Flex.Justify.END} 86 + > 87 + {ext.state === ExtensionState.NotDownloaded ? ( 88 + <Button 89 + color={Button.Colors.BRAND} 90 + submitting={busy} 91 + onClick={() => { 92 + MoonbaseSettingsStore.installExtension(id); 93 + }} 94 + > 95 + Install 96 + </Button> 97 + ) : ( 98 + <div 99 + // too lazy to learn how <Flex /> works lmao 100 + style={{ 101 + display: "flex", 102 + alignItems: "center", 103 + gap: "1rem" 104 + }} 105 + > 106 + {ext.source.type == ExtensionLoadSource.Normal && ( 107 + // TODO: this needs centering 108 + <Button 109 + color={Button.Colors.RED} 110 + size={Button.Sizes.ICON} 111 + submitting={busy} 112 + onClick={() => { 113 + MoonbaseSettingsStore.deleteExtension(id); 114 + }} 115 + > 116 + <TrashIcon width={27} /> 117 + </Button> 118 + )} 119 + 120 + {update != null && ( 121 + <Button 122 + color={Button.Colors.BRAND} 123 + size={Button.Sizes.ICON} 124 + submitting={busy} 125 + onClick={() => { 126 + MoonbaseSettingsStore.installExtension(id); 127 + }} 128 + > 129 + <DownloadIcon width={27} /> 130 + </Button> 131 + )} 132 + 133 + <Switch 134 + checked={enabled} 135 + onChange={() => { 136 + MoonbaseSettingsStore.setExtensionEnabled(id, !enabled); 137 + }} 138 + /> 139 + </div> 140 + )} 141 + </Flex> 142 + </div> 143 + 144 + <div className={UserProfileClasses.body}> 145 + {(description != null || settings != null) && ( 146 + <div 147 + className={UserProfileClasses.tabBarContainer} 148 + style={{ 149 + padding: "0 10px" 150 + }} 151 + > 152 + <TabBar 153 + selectedItem={tab} 154 + type="top" 155 + onItemSelect={setTab} 156 + className={UserProfileClasses.tabBar} 157 + > 158 + <TabBar.Item 159 + className={UserProfileClasses.tabBarItem} 160 + id={ExtensionPage.Info} 161 + > 162 + Info 163 + </TabBar.Item> 164 + 165 + {description != null && ( 166 + <TabBar.Item 167 + className={UserProfileClasses.tabBarItem} 168 + id={ExtensionPage.Description} 169 + > 170 + Description 171 + </TabBar.Item> 172 + )} 173 + 174 + {settings != null && ( 175 + <TabBar.Item 176 + className={UserProfileClasses.tabBarItem} 177 + id={ExtensionPage.Settings} 178 + > 179 + Settings 180 + </TabBar.Item> 181 + )} 182 + </TabBar> 183 + </div> 184 + )} 185 + 186 + <Flex 187 + justify={Flex.Justify.START} 188 + wrap={Flex.Wrap.WRAP} 189 + style={{ 190 + padding: "16px 16px" 191 + }} 192 + > 193 + {tab === ExtensionPage.Info && <ExtensionInfo ext={ext} />} 194 + {tab === ExtensionPage.Description && ( 195 + <Text variant="text-md/normal"> 196 + {MarkdownParser.parse(description ?? "*No description*")} 197 + </Text> 198 + )} 199 + {tab === ExtensionPage.Settings && <Settings ext={ext} />} 200 + </Flex> 201 + </div> 202 + </Card> 203 + ); 204 + } 205 + 206 + return function Moonbase() { 207 + const { extensions } = Flux.useStateFromStoresObject( 208 + [MoonbaseSettingsStore], 209 + () => { 210 + return { extensions: MoonbaseSettingsStore.extensions }; 211 + } 212 + ); 213 + 214 + const sorted = Object.values(extensions).sort((a, b) => { 215 + const aName = a.manifest.meta?.name ?? a.id; 216 + const bName = b.manifest.meta?.name ?? b.id; 217 + return aName.localeCompare(bName); 218 + }); 219 + 220 + return ( 221 + <> 222 + {sorted.map((ext) => ( 223 + <ExtensionCard id={ext.id} key={ext.id} /> 224 + ))} 225 + </> 226 + ); 227 + }; 228 + };
+198
packages/core-extensions/src/moonbase/ui/info.tsx
··· 1 + import WebpackRequire from "@moonlight-mod/types/discord/require"; 2 + import { DetectedExtension, ExtensionTag } from "@moonlight-mod/types"; 3 + import { MoonbaseExtension } from "../types"; 4 + 5 + type Dependency = { 6 + id: string; 7 + type: DependencyType; 8 + }; 9 + 10 + enum DependencyType { 11 + Dependency = "dependency", 12 + Optional = "optional", 13 + Incompatible = "incompatible" 14 + } 15 + 16 + export default (require: typeof WebpackRequire) => { 17 + const React = require("common_react"); 18 + const spacepack = require("spacepack_spacepack"); 19 + 20 + const CommonComponents = require("common_components"); 21 + const UserInfoClasses = spacepack.findByCode( 22 + "infoScroller", 23 + "userInfoSection", 24 + "userInfoSectionHeader" 25 + )[0].exports; 26 + 27 + const { MoonbaseSettingsStore } = require("moonbase_stores") as ReturnType< 28 + typeof import("../stores")["stores"] 29 + >; 30 + 31 + function InfoSection({ 32 + title, 33 + children 34 + }: { 35 + title: string; 36 + children: React.ReactNode; 37 + }) { 38 + return ( 39 + <div 40 + style={{ 41 + marginRight: "1em" 42 + }} 43 + > 44 + <CommonComponents.Text 45 + variant="eyebrow" 46 + className={UserInfoClasses.userInfoSectionHeader} 47 + > 48 + {title} 49 + </CommonComponents.Text> 50 + 51 + <CommonComponents.Text variant="text-sm/normal"> 52 + {children} 53 + </CommonComponents.Text> 54 + </div> 55 + ); 56 + } 57 + 58 + function Badge({ 59 + color, 60 + children 61 + }: { 62 + color: string; 63 + children: React.ReactNode; 64 + }) { 65 + return ( 66 + <span 67 + style={{ 68 + borderRadius: ".1875rem", 69 + padding: "0 0.275rem", 70 + marginRight: "0.4em", 71 + backgroundColor: color, 72 + color: "#fff" 73 + }} 74 + > 75 + {children} 76 + </span> 77 + ); 78 + } 79 + 80 + function ExtensionInfo({ ext }: { ext: MoonbaseExtension }) { 81 + const { Flex, Text } = CommonComponents; 82 + const authors = ext.manifest?.meta?.authors; 83 + const tags = ext.manifest?.meta?.tags; 84 + 85 + const dependencies: Dependency[] = []; 86 + if (ext.manifest.dependencies != null) { 87 + dependencies.push( 88 + ...ext.manifest.dependencies.map((dep) => ({ 89 + id: dep, 90 + type: DependencyType.Dependency 91 + })) 92 + ); 93 + } 94 + 95 + if (ext.manifest.suggested != null) { 96 + dependencies.push( 97 + ...ext.manifest.suggested.map((dep) => ({ 98 + id: dep, 99 + type: DependencyType.Optional 100 + })) 101 + ); 102 + } 103 + 104 + if (ext.manifest.incompatible != null) { 105 + dependencies.push( 106 + ...ext.manifest.incompatible.map((dep) => ({ 107 + id: dep, 108 + type: DependencyType.Incompatible 109 + })) 110 + ); 111 + } 112 + 113 + return ( 114 + <> 115 + {authors != null && ( 116 + <InfoSection title="Authors"> 117 + {authors.map((author, i) => { 118 + const comma = i !== authors.length - 1 ? ", " : ""; 119 + if (typeof author === "string") { 120 + return ( 121 + <span> 122 + {author} 123 + {comma} 124 + </span> 125 + ); 126 + } else { 127 + // TODO: resolve IDs 128 + return ( 129 + <span> 130 + {author.name} 131 + {comma} 132 + </span> 133 + ); 134 + } 135 + })} 136 + </InfoSection> 137 + )} 138 + 139 + {tags != null && ( 140 + <InfoSection title="Tags"> 141 + {tags.map((tag, i) => { 142 + const names: Record<ExtensionTag, string> = { 143 + [ExtensionTag.Accessibility]: "Accessibility", 144 + [ExtensionTag.Appearance]: "Appearance", 145 + [ExtensionTag.Chat]: "Chat", 146 + [ExtensionTag.Commands]: "Commands", 147 + [ExtensionTag.ContextMenu]: "Context Menu", 148 + [ExtensionTag.DangerZone]: "Danger Zone", 149 + [ExtensionTag.Development]: "Development", 150 + [ExtensionTag.Fixes]: "Fixes", 151 + [ExtensionTag.Fun]: "Fun", 152 + [ExtensionTag.Markdown]: "Markdown", 153 + [ExtensionTag.Voice]: "Voice", 154 + [ExtensionTag.Privacy]: "Privacy", 155 + [ExtensionTag.Profiles]: "Profiles", 156 + [ExtensionTag.QualityOfLife]: "Quality of Life", 157 + [ExtensionTag.Library]: "Library" 158 + }; 159 + const name = names[tag]; 160 + 161 + return ( 162 + <Badge 163 + color={ 164 + tag == ExtensionTag.DangerZone 165 + ? "var(--red-400)" 166 + : "var(--brand-500)" 167 + } 168 + > 169 + {name} 170 + </Badge> 171 + ); 172 + })} 173 + </InfoSection> 174 + )} 175 + 176 + {dependencies.length > 0 && ( 177 + <InfoSection title="Dependencies"> 178 + {dependencies.map((dep) => { 179 + const colors = { 180 + [DependencyType.Dependency]: "var(--brand-500)", 181 + [DependencyType.Optional]: "var(--orange-400)", 182 + [DependencyType.Incompatible]: "var(--red-400)" 183 + }; 184 + const color = colors[dep.type]; 185 + const name = MoonbaseSettingsStore.getExtensionName(dep.id); 186 + return <Badge color={color}>{name}</Badge>; 187 + })} 188 + </InfoSection> 189 + )} 190 + </> 191 + ); 192 + } 193 + 194 + return { 195 + InfoSection, 196 + ExtensionInfo 197 + }; 198 + };
+327
packages/core-extensions/src/moonbase/ui/settings.tsx
··· 1 + import { 2 + DictionarySettingType, 3 + ExtensionSettingType, 4 + ExtensionSettingsManifest, 5 + NumberSettingType, 6 + SelectSettingType 7 + } from "@moonlight-mod/types/config"; 8 + import WebpackRequire from "@moonlight-mod/types/discord/require"; 9 + import { MoonbaseExtension } from "../types"; 10 + 11 + type SettingsProps = { 12 + ext: MoonbaseExtension; 13 + name: string; 14 + setting: ExtensionSettingsManifest; 15 + }; 16 + 17 + type SettingsComponent = React.ComponentType<SettingsProps>; 18 + 19 + export default (require: typeof WebpackRequire) => { 20 + const React = require("common_react"); 21 + const spacepack = require("spacepack_spacepack"); 22 + const CommonComponents = require("common_components"); 23 + const Flux = require("common_flux"); 24 + 25 + const { MoonbaseSettingsStore } = require("moonbase_stores") as ReturnType< 26 + typeof import("../stores")["stores"] 27 + >; 28 + 29 + function Boolean({ ext, name, setting }: SettingsProps) { 30 + const { FormSwitch } = CommonComponents; 31 + const { value, displayName } = Flux.useStateFromStores( 32 + [MoonbaseSettingsStore], 33 + () => { 34 + return { 35 + value: MoonbaseSettingsStore.getExtensionConfig<boolean>( 36 + ext.id, 37 + name 38 + ), 39 + displayName: MoonbaseSettingsStore.getExtensionConfigName( 40 + ext.id, 41 + name 42 + ) 43 + }; 44 + }, 45 + [ext.id, name] 46 + ); 47 + 48 + return ( 49 + <FormSwitch 50 + value={value ?? false} 51 + hideBorder={true} 52 + onChange={(value: boolean) => { 53 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value); 54 + }} 55 + > 56 + {displayName} 57 + </FormSwitch> 58 + ); 59 + } 60 + 61 + function Number({ ext, name, setting }: SettingsProps) { 62 + const { Slider, ControlClasses } = CommonComponents; 63 + const { value, displayName } = Flux.useStateFromStores( 64 + [MoonbaseSettingsStore], 65 + () => { 66 + return { 67 + value: MoonbaseSettingsStore.getExtensionConfig<number>(ext.id, name), 68 + displayName: MoonbaseSettingsStore.getExtensionConfigName( 69 + ext.id, 70 + name 71 + ) 72 + }; 73 + }, 74 + [ext.id, name] 75 + ); 76 + 77 + const castedSetting = setting as NumberSettingType; 78 + const min = castedSetting.min ?? 0; 79 + const max = castedSetting.max ?? 100; 80 + 81 + return ( 82 + <div> 83 + <label className={ControlClasses.title}>{displayName}</label> 84 + <Slider 85 + initialValue={value ?? 0} 86 + minValue={castedSetting.min ?? 0} 87 + maxValue={castedSetting.max ?? 100} 88 + onValueChange={(value: number) => { 89 + const rounded = Math.max(min, Math.min(max, Math.round(value))); 90 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, rounded); 91 + }} 92 + /> 93 + </div> 94 + ); 95 + } 96 + 97 + function String({ ext, name, setting }: SettingsProps) { 98 + const { TextInput, ControlClasses } = CommonComponents; 99 + const { value, displayName } = Flux.useStateFromStores( 100 + [MoonbaseSettingsStore], 101 + () => { 102 + return { 103 + value: MoonbaseSettingsStore.getExtensionConfig<string>(ext.id, name), 104 + displayName: MoonbaseSettingsStore.getExtensionConfigName( 105 + ext.id, 106 + name 107 + ) 108 + }; 109 + }, 110 + [ext.id, name] 111 + ); 112 + 113 + return ( 114 + <div> 115 + <label className={ControlClasses.title}>{displayName}</label> 116 + <TextInput 117 + value={value ?? ""} 118 + onChange={(value: string) => { 119 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value); 120 + }} 121 + /> 122 + </div> 123 + ); 124 + } 125 + 126 + function Select({ ext, name, setting }: SettingsProps) { 127 + const { ControlClasses, SingleSelect } = CommonComponents; 128 + const { value, displayName } = Flux.useStateFromStores( 129 + [MoonbaseSettingsStore], 130 + () => { 131 + return { 132 + value: MoonbaseSettingsStore.getExtensionConfig<string>(ext.id, name), 133 + displayName: MoonbaseSettingsStore.getExtensionConfigName( 134 + ext.id, 135 + name 136 + ) 137 + }; 138 + }, 139 + [ext.id, name] 140 + ); 141 + 142 + const castedSetting = setting as SelectSettingType; 143 + const options = castedSetting.options; 144 + 145 + return ( 146 + <div> 147 + <label className={ControlClasses.title}>{displayName}</label> 148 + <SingleSelect 149 + autofocus={false} 150 + clearable={false} 151 + value={value ?? ""} 152 + options={options.map((o) => ({ value: o, label: o }))} 153 + onChange={(value: string) => { 154 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value); 155 + }} 156 + /> 157 + </div> 158 + ); 159 + } 160 + 161 + function List({ ext, name, setting }: SettingsProps) { 162 + const { ControlClasses, Select, useVariableSelect, multiSelect } = 163 + CommonComponents; 164 + const { value, displayName } = Flux.useStateFromStores( 165 + [MoonbaseSettingsStore], 166 + () => { 167 + return { 168 + value: 169 + MoonbaseSettingsStore.getExtensionConfig<string>(ext.id, name) ?? 170 + [], 171 + displayName: MoonbaseSettingsStore.getExtensionConfigName( 172 + ext.id, 173 + name 174 + ) 175 + }; 176 + }, 177 + [ext.id, name] 178 + ); 179 + 180 + const castedSetting = setting as SelectSettingType; 181 + const options = castedSetting.options; 182 + 183 + return ( 184 + <div> 185 + <label className={ControlClasses.title}>{displayName}</label> 186 + <Select 187 + autofocus={false} 188 + clearable={false} 189 + options={options.map((o) => ({ value: o, label: o }))} 190 + {...useVariableSelect({ 191 + onSelectInteraction: multiSelect, 192 + value: new Set(Array.isArray(value) ? value : [value]), 193 + onChange: (value: string) => { 194 + MoonbaseSettingsStore.setExtensionConfig( 195 + ext.id, 196 + name, 197 + Array.from(value) 198 + ); 199 + } 200 + })} 201 + /> 202 + </div> 203 + ); 204 + } 205 + 206 + function Dictionary({ ext, name, setting }: SettingsProps) { 207 + const { TextInput, ControlClasses, Button, Flex } = CommonComponents; 208 + const { value, displayName } = Flux.useStateFromStores( 209 + [MoonbaseSettingsStore], 210 + () => { 211 + return { 212 + value: MoonbaseSettingsStore.getExtensionConfig< 213 + Record<string, string> 214 + >(ext.id, name), 215 + displayName: MoonbaseSettingsStore.getExtensionConfigName( 216 + ext.id, 217 + name 218 + ) 219 + }; 220 + }, 221 + [ext.id, name] 222 + ); 223 + 224 + const castedSetting = setting as DictionarySettingType; 225 + const entries = Object.entries(value ?? {}); 226 + 227 + return ( 228 + <Flex direction={Flex.Direction.VERTICAL}> 229 + <label className={ControlClasses.title}>{displayName}</label> 230 + {entries.map(([key, val], i) => ( 231 + // FIXME: stylesheets 232 + <div 233 + key={i} 234 + style={{ 235 + display: "grid", 236 + height: "40px", 237 + gap: "10px", 238 + gridTemplateColumns: "1fr 1fr 40px" 239 + }} 240 + > 241 + <TextInput 242 + value={key} 243 + onChange={(newKey: string) => { 244 + entries[i][0] = newKey; 245 + MoonbaseSettingsStore.setExtensionConfig( 246 + ext.id, 247 + name, 248 + Object.fromEntries(entries) 249 + ); 250 + }} 251 + /> 252 + <TextInput 253 + value={val} 254 + onChange={(newValue: string) => { 255 + entries[i][1] = newValue; 256 + MoonbaseSettingsStore.setExtensionConfig( 257 + ext.id, 258 + name, 259 + Object.fromEntries(entries) 260 + ); 261 + }} 262 + /> 263 + <Button 264 + color={Button.Colors.RED} 265 + size={Button.Sizes.ICON} 266 + onClick={() => { 267 + entries.splice(i, 1); 268 + MoonbaseSettingsStore.setExtensionConfig( 269 + ext.id, 270 + name, 271 + Object.fromEntries(entries) 272 + ); 273 + }} 274 + > 275 + X 276 + </Button> 277 + </div> 278 + ))} 279 + 280 + <Button 281 + look={Button.Looks.FILLED} 282 + color={Button.Colors.GREEN} 283 + onClick={() => { 284 + entries.push([`entry-${entries.length}`, ""]); 285 + MoonbaseSettingsStore.setExtensionConfig( 286 + ext.id, 287 + name, 288 + Object.fromEntries(entries) 289 + ); 290 + }} 291 + > 292 + Add new entry 293 + </Button> 294 + </Flex> 295 + ); 296 + } 297 + 298 + function Setting({ ext, name, setting }: SettingsProps) { 299 + const elements: Partial<Record<ExtensionSettingType, SettingsComponent>> = { 300 + [ExtensionSettingType.Boolean]: Boolean, 301 + [ExtensionSettingType.Number]: Number, 302 + [ExtensionSettingType.String]: String, 303 + [ExtensionSettingType.Select]: Select, 304 + [ExtensionSettingType.List]: List, 305 + [ExtensionSettingType.Dictionary]: Dictionary 306 + }; 307 + const element = elements[setting.type]; 308 + if (element == null) return <></>; 309 + return React.createElement(element, { ext, name, setting }); 310 + } 311 + 312 + function Settings({ ext }: { ext: MoonbaseExtension }) { 313 + const { Flex } = CommonComponents; 314 + return ( 315 + <Flex direction={Flex.Direction.VERTICAL}> 316 + {Object.entries(ext.manifest.settings!).map(([name, setting]) => ( 317 + <Setting ext={ext} key={name} name={name} setting={setting} /> 318 + ))} 319 + </Flex> 320 + ); 321 + } 322 + 323 + return { 324 + Boolean, 325 + Settings 326 + }; 327 + };
+11
packages/core-extensions/src/noHideToken/index.ts
··· 1 + import { Patch } from "types/src"; 2 + 3 + export const patches: Patch[] = [ 4 + { 5 + find: "hideToken(){", 6 + replace: { 7 + match: /hideToken\(\)\{.+?},/, 8 + replacement: `hideToken(){},` 9 + } 10 + } 11 + ];
+9
packages/core-extensions/src/noHideToken/manifest.json
··· 1 + { 2 + "id": "noHideToken", 3 + "meta": { 4 + "name": "No Hide Token", 5 + "tagline": "Disables removal of token from localStorage when opening dev tools", 6 + "authors": ["adryd"], 7 + "tags": ["dangerZone", "development"] 8 + } 9 + }
+15
packages/core-extensions/src/noTrack/host.ts
··· 1 + import { BrowserWindow } from "electron"; 2 + 3 + moonlightHost.events.on("window-created", (window: BrowserWindow) => { 4 + window.webContents.session.webRequest.onBeforeRequest( 5 + { 6 + urls: [ 7 + "https://*.discord.com/api/v*/science", 8 + "https://*.discord.com/api/v*/metrics" 9 + ] 10 + }, 11 + function (details, callback) { 12 + callback({ cancel: true }); 13 + } 14 + ); 15 + });
+18
packages/core-extensions/src/noTrack/index.ts
··· 1 + import { Patch, PatchReplaceType } from "@moonlight-mod/types"; 2 + 3 + export const patches: Patch[] = [ 4 + { 5 + find: "analyticsTrackingStoreMaker:function", 6 + replace: { 7 + match: /analyticsTrackingStoreMaker:function\(\){return .}/, 8 + replacement: "analyticsTrackingStoreMaker:function(){return ()=>{}}" 9 + } 10 + }, 11 + { 12 + find: /this\._metrics\.push\(.\),/, 13 + replace: { 14 + match: /this\._metrics\.push\(.\),/, 15 + replacement: "" 16 + } 17 + } 18 + ];
+9
packages/core-extensions/src/noTrack/manifest.json
··· 1 + { 2 + "id": "noTrack", 3 + "meta": { 4 + "name": "No Track", 5 + "tagline": "Disables /api/science and analytics", 6 + "authors": ["Cynosphere", "NotNite"], 7 + "tags": ["privacy"] 8 + } 9 + }
+111
packages/core-extensions/src/quietLoggers/index.ts
··· 1 + import { Patch } from "@moonlight-mod/types"; 2 + 3 + const notXssDefensesOnly = () => 4 + (moonlight.getConfigOption<boolean>("quietLoggers", "xssDefensesOnly") ?? 5 + false) === false; 6 + 7 + // These patches MUST run before the simple patches, these are to remove loggers 8 + // that end up causing syntax errors by the normal patch 9 + const loggerFixes: Patch[] = [ 10 + { 11 + find: '"./ggsans-800-extrabolditalic.woff2":', 12 + replace: { 13 + match: /\.then\(function\(\){var.+?"MODULE_NOT_FOUND",.\}\)/, 14 + replacement: ".then(()=>(()=>{}))" 15 + } 16 + }, 17 + { 18 + find: '("GatewaySocket")', 19 + replace: { 20 + match: /.\.(info|log)(\(.+?\))(;|,)/g, 21 + replacement: (_, type, body, trail) => `(()=>{})${body}${trail}` 22 + } 23 + } 24 + ]; 25 + loggerFixes.forEach((patch) => { 26 + patch.prerequisite = notXssDefensesOnly; 27 + }); 28 + 29 + // Patches to simply remove a logger call 30 + const stubPatches = [ 31 + // "sh" is not a valid locale. 32 + [ 33 + "is not a valid locale", 34 + /(.)\.error\(""\.concat\((.)\," is not a valid locale\."\)\)/g 35 + ], 36 + ['.displayName="RunningGameStore"', /.\.info\("games",{.+?}\),/], 37 + [ 38 + '"[BUILD INFO] Release Channel: "', 39 + /new\(0,.{1,2}\.default\)\(\)\.log\("\[BUILD INFO\] Release Channel: ".+?"\)\),/ 40 + ], 41 + [ 42 + '.AnalyticEvents.APP_NATIVE_CRASH,"Storage"', 43 + /console\.log\("AppCrashedFatalReport lastCrash:",.,.\);/ 44 + ], 45 + [ 46 + '.AnalyticEvents.APP_NATIVE_CRASH,"Storage"', 47 + 'console.log("AppCrashedFatalReport: getLastCrash not supported.");' 48 + ], 49 + [ 50 + '"[NATIVE INFO] ', 51 + /new\(0,.{1,2}\.default\)\(\)\.log\("\[NATIVE INFO] .+?\)\),/ 52 + ], 53 + ['"Spellchecker"', /.\.info\("Switching to ".+?"\(unavailable\)"\);?/g], 54 + [ 55 + 'throw new Error("Messages are still loading.");', 56 + /console\.warn\("Unsupported Locale",.\);/ 57 + ], 58 + ["_dispatchWithDevtools=", /.\.has\(.\.type\)&&.\.log\(.+?\);/], 59 + ["_dispatchWithDevtools=", /.\.totalTime>100&&.\.log\(.+?\);0;/], 60 + [ 61 + '"NativeDispatchUtils"', 62 + /null==.&&.\.warn\("Tried getting Dispatch instance before instantiated"\),/ 63 + ], 64 + [ 65 + 'Error("Messages are still loading.")', 66 + /console\.warn\("Unsupported Locale",.\),/ 67 + ], 68 + ['("DatabaseManager")', /.\.log\("removing database \(user: ".+?\)\),/], 69 + [ 70 + '"Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. Action: "', 71 + /.\.has\(.\.type\)&&.\.log\(.+?\.type\)\),/ 72 + ] 73 + ]; 74 + 75 + const simplePatches = [ 76 + // Moment.js deprecation warnings 77 + ["suppressDeprecationWarnings=!1", "suppressDeprecationWarnings=!0"], 78 + 79 + // Zustand related 80 + [ 81 + /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\)/g, 82 + "/*$&*/" 83 + ] 84 + ] as { [0]: string | RegExp; [1]: string }[]; 85 + 86 + export const patches: Patch[] = [ 87 + { 88 + find: ".Messages.XSSDefenses", 89 + replace: { 90 + match: /\(null!=.{1,2}&&"0\.0\.0"===.{1,2}\.remoteApp\.getVersion\(\)\)/, 91 + replacement: "(true)" 92 + } 93 + }, 94 + ...loggerFixes, 95 + ...stubPatches.map((patch) => ({ 96 + find: patch[0], 97 + replace: { 98 + match: patch[1], 99 + replacement: "" 100 + }, 101 + prerequisite: notXssDefensesOnly 102 + })), 103 + ...simplePatches.map((patch) => ({ 104 + find: patch[0], 105 + replace: { 106 + match: patch[0], 107 + replacement: patch[1] 108 + }, 109 + prerequisite: notXssDefensesOnly 110 + })) 111 + ];
+17
packages/core-extensions/src/quietLoggers/manifest.json
··· 1 + { 2 + "id": "quietLoggers", 3 + "meta": { 4 + "name": "Quiet Loggers", 5 + "tagline": "Quiet errors on startup, and disable unnecesary loggers", 6 + "authors": ["Cynosphere", "NotNite", "adryd"], 7 + "tags": ["development"] 8 + }, 9 + "settings": { 10 + "xssDefensesOnly": { 11 + "displayName": "Only hide self-XSS", 12 + "description": "Only disable self XSS prevention log", 13 + "type": "boolean", 14 + "default": false 15 + } 16 + } 17 + }
+78
packages/core-extensions/src/settings/index.ts
··· 1 + import { Patch, PatchReplaceType } from "@moonlight-mod/types"; 2 + import { 3 + SettingsSection, 4 + Settings as SettingsType 5 + } from "@moonlight-mod/types/coreExtensions"; 6 + import { ExtensionWebExports, WebpackModuleFunc } from "@moonlight-mod/types"; 7 + 8 + export const patches: Patch[] = [ 9 + { 10 + find: ".UserSettingsSections.EXPERIMENTS", 11 + replace: { 12 + match: /\.CUSTOM,element:(.+?)}\];return (.{1,2})/, 13 + replacement: (_, lastElement, sections) => 14 + `.CUSTOM,element:${lastElement}}];return require("settings_settings")._mutateSections(${sections})` 15 + } 16 + }, 17 + { 18 + find: 'navId:"user-settings-cog",', 19 + replace: { 20 + match: /children:\[(.)\.map\(.+?\),{children:.\((.)\)/, 21 + replacement: (orig, sections, section) => 22 + `${orig}??${sections}.find(x=>x.section==${section})?._moonlight_submenu?.()` 23 + } 24 + } 25 + ]; 26 + 27 + export const webpackModules: ExtensionWebExports["webpackModules"] = { 28 + settings: { 29 + run: (module, exports, require) => { 30 + const Settings: SettingsType = { 31 + ourSections: [], 32 + 33 + addSection: (section, label, element, color = null, pos, notice) => { 34 + const data: SettingsSection = { 35 + section, 36 + label, 37 + color, 38 + element, 39 + pos: pos ?? -4, 40 + notice: notice 41 + }; 42 + 43 + Settings.ourSections.push(data); 44 + return data; 45 + }, 46 + 47 + addDivider: (pos = null) => { 48 + Settings.ourSections.push({ 49 + section: "DIVIDER", 50 + pos: pos === null ? -4 : pos 51 + }); 52 + }, 53 + 54 + addHeader: function (label, pos = null) { 55 + Settings.ourSections.push({ 56 + section: "HEADER", 57 + label: label, 58 + pos: pos === null ? -4 : pos 59 + }); 60 + }, 61 + 62 + _mutateSections: (sections) => { 63 + for (const section of Settings.ourSections) { 64 + sections.splice( 65 + section.pos < 0 ? sections.length + section.pos : section.pos, 66 + 0, 67 + section 68 + ); 69 + } 70 + 71 + return sections; 72 + } 73 + }; 74 + 75 + module.exports = Settings; 76 + } 77 + } 78 + };
+9
packages/core-extensions/src/settings/manifest.json
··· 1 + { 2 + "id": "settings", 3 + "meta": { 4 + "name": "Settings", 5 + "tagline": "An API for adding to Discord's settings menu", 6 + "authors": ["Cynosphere", "NotNite"], 7 + "tags": ["library"] 8 + } 9 + }
+10
packages/core-extensions/src/spacepack/index.ts
··· 1 + import { ExtensionWebExports, WebpackModuleFunc } from "@moonlight-mod/types"; 2 + import webpackModule from "./webpackModule"; 3 + 4 + export const webpackModules: ExtensionWebExports["webpackModules"] = { 5 + spacepack: { 6 + entrypoint: true, 7 + // Assert the type because we're adding extra fields to require 8 + run: webpackModule as WebpackModuleFunc 9 + } 10 + };
+17
packages/core-extensions/src/spacepack/manifest.json
··· 1 + { 2 + "id": "spacepack", 3 + "meta": { 4 + "name": "Spacepack", 5 + "tagline": "Search utilities across all Webpack modules", 6 + "authors": ["Cynosphere", "NotNite"], 7 + "tags": ["library", "development"] 8 + }, 9 + "settings": { 10 + "addToGlobalScope": { 11 + "displayName": "Add to global scope", 12 + "description": "Populates window.spacepack for easier usage in DevTools", 13 + "type": "boolean", 14 + "default": false 15 + } 16 + } 17 + }
+172
packages/core-extensions/src/spacepack/webpackModule.ts
··· 1 + import { WebpackModuleFunc, WebpackModule } from "@moonlight-mod/types"; 2 + import { Spacepack } from "@moonlight-mod/types/coreExtensions"; 3 + import { WebpackRequireType } from "@moonlight-mod/types/discord/webpack"; 4 + 5 + declare global { 6 + interface Window { 7 + spacepack: Spacepack; 8 + } 9 + } 10 + 11 + export default (module: any, exports: any, require: WebpackRequireType) => { 12 + const cache = require.c; 13 + const modules = require.m; 14 + 15 + const spacepack: Spacepack = { 16 + require, 17 + modules, 18 + cache, 19 + 20 + inspect: (module: number | string) => { 21 + if (typeof module === "number") { 22 + module = module.toString(); 23 + } 24 + 25 + if (!(module in modules)) { 26 + return null; 27 + } 28 + 29 + const func = modules[module]; 30 + if (func.__moonlight === true) { 31 + return func; 32 + } 33 + 34 + const funcStr = func.toString(); 35 + 36 + return new Function( 37 + "module", 38 + "exports", 39 + "require", 40 + `(${funcStr}).apply(this, arguments)\n` + 41 + `//# sourceURL=Webpack-Module-${module}` 42 + ) as WebpackModuleFunc; 43 + }, 44 + 45 + findByCode: (...args: (string | RegExp)[]) => { 46 + return Object.entries(modules) 47 + .filter( 48 + ([id, mod]) => 49 + !args.some( 50 + (item) => 51 + !(item instanceof RegExp 52 + ? item.test(mod.toString()) 53 + : mod.toString().indexOf(item) !== -1) 54 + ) 55 + ) 56 + .map(([id]) => { 57 + //if (!(id in cache)) require(id); 58 + //return cache[id]; 59 + 60 + let exports; 61 + try { 62 + exports = require(id); 63 + } catch (e) { 64 + console.error(e); 65 + debugger; 66 + } 67 + 68 + return { 69 + id, 70 + exports 71 + }; 72 + }) 73 + .filter((item) => item != null); 74 + }, 75 + 76 + findByExports: (...args: string[]) => { 77 + return Object.entries(cache) 78 + .filter( 79 + ([id, { exports }]) => 80 + !args.some( 81 + (item) => 82 + !( 83 + exports != undefined && 84 + exports != window && 85 + (exports?.[item] || 86 + exports?.default?.[item] || 87 + exports?.Z?.[item] || 88 + exports?.ZP?.[item]) 89 + ) 90 + ) 91 + ) 92 + .map((item) => item[1]) 93 + .reduce<WebpackModule[]>((prev, curr) => { 94 + if (!prev.includes(curr)) prev.push(curr); 95 + return prev; 96 + }, []); 97 + }, 98 + 99 + findObjectFromKey: (exports: Record<string, any>, key: string) => { 100 + let subKey; 101 + if (key.indexOf(".") > -1) { 102 + const splitKey = key.split("."); 103 + key = splitKey[0]; 104 + subKey = splitKey[1]; 105 + } 106 + for (const exportKey in exports) { 107 + const obj = exports[exportKey]; 108 + if (obj && obj[key] !== undefined) { 109 + if (subKey) { 110 + if (obj[key][subKey]) return obj; 111 + } else { 112 + return obj; 113 + } 114 + } 115 + } 116 + return null; 117 + }, 118 + 119 + findObjectFromValue: (exports: Record<string, any>, value: any) => { 120 + for (const exportKey in exports) { 121 + const obj = exports[exportKey]; 122 + if (obj == value) return obj; 123 + for (const subKey in obj) { 124 + if (obj && obj[subKey] == value) { 125 + return obj; 126 + } 127 + } 128 + } 129 + return null; 130 + }, 131 + 132 + findObjectFromKeyValuePair: ( 133 + exports: Record<string, any>, 134 + key: string, 135 + value: any 136 + ) => { 137 + for (const exportKey in exports) { 138 + const obj = exports[exportKey]; 139 + if (obj && obj[key] == value) { 140 + return obj; 141 + } 142 + } 143 + return null; 144 + }, 145 + 146 + findFunctionByStrings: ( 147 + exports: Record<string, any>, 148 + ...strings: (string | RegExp)[] 149 + ) => { 150 + return ( 151 + Object.entries(exports).filter( 152 + ([index, func]) => 153 + typeof func === "function" && 154 + !strings.some( 155 + (query) => 156 + !(query instanceof RegExp 157 + ? func.toString().match(query) 158 + : func.toString().includes(query)) 159 + ) 160 + )?.[0]?.[1] ?? null 161 + ); 162 + } 163 + }; 164 + 165 + module.exports = spacepack; 166 + 167 + if ( 168 + moonlight.getConfigOption<boolean>("spacepack", "addToGlobalScope") === true 169 + ) { 170 + window.spacepack = spacepack; 171 + } 172 + };
+3
packages/core-extensions/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json" 3 + }
+11
packages/core/package.json
··· 1 + { 2 + "name": "@moonlight-mod/core", 3 + "private": true, 4 + "exports": { 5 + "./*": "./src/*.ts" 6 + }, 7 + "dependencies": { 8 + "glob": "^10.3.4", 9 + "@moonlight-mod/types": "workspace:*" 10 + } 11 + }
+48
packages/core/src/config.ts
··· 1 + import { Config, constants } from "@moonlight-mod/types"; 2 + import requireImport from "./util/import"; 3 + import { getConfigPath } from "./util/data"; 4 + 5 + const defaultConfig: Config = { 6 + extensions: {}, 7 + repositories: [] 8 + }; 9 + 10 + export function writeConfig(config: Config) { 11 + const fs = requireImport("fs"); 12 + const configPath = getConfigPath(); 13 + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 14 + } 15 + 16 + function readConfigNode(): Config { 17 + const fs = requireImport("fs"); 18 + const configPath = getConfigPath(); 19 + 20 + if (!fs.existsSync(configPath)) { 21 + writeConfig(defaultConfig); 22 + return defaultConfig; 23 + } 24 + 25 + let config: Config = JSON.parse(fs.readFileSync(configPath, "utf8")); 26 + 27 + // Assign the default values if they don't exist (newly added) 28 + config = { ...defaultConfig, ...config }; 29 + writeConfig(config); 30 + 31 + return config; 32 + } 33 + 34 + export function readConfig(): Config { 35 + webPreload: { 36 + return moonlightNode.config; 37 + } 38 + 39 + nodePreload: { 40 + return readConfigNode(); 41 + } 42 + 43 + injector: { 44 + return readConfigNode(); 45 + } 46 + 47 + throw new Error("Called readConfig() in an impossible environment"); 48 + }
+107
packages/core/src/extension.ts
··· 1 + import { 2 + ExtensionManifest, 3 + DetectedExtension, 4 + ExtensionLoadSource, 5 + constants 6 + } from "@moonlight-mod/types"; 7 + import { readConfig } from "./config"; 8 + import requireImport from "./util/import"; 9 + import { getCoreExtensionsPath, getExtensionsPath } from "./util/data"; 10 + 11 + function loadDetectedExtensions( 12 + dir: string, 13 + type: ExtensionLoadSource 14 + ): DetectedExtension[] { 15 + const fs = requireImport("fs"); 16 + const path = requireImport("path"); 17 + const ret: DetectedExtension[] = []; 18 + 19 + const glob = require("glob"); 20 + const manifests = glob.sync(dir + "/**/manifest.json"); 21 + 22 + for (const manifestPath of manifests) { 23 + if (!fs.existsSync(manifestPath)) continue; 24 + const dir = path.dirname(manifestPath); 25 + 26 + const manifest: ExtensionManifest = JSON.parse( 27 + fs.readFileSync(manifestPath, "utf8") 28 + ); 29 + 30 + const webPath = path.join(dir, "index.js"); 31 + const nodePath = path.join(dir, "node.js"); 32 + const hostPath = path.join(dir, "host.js"); 33 + 34 + // if none exist (empty manifest) don't give a shit 35 + if ( 36 + !fs.existsSync(webPath) && 37 + !fs.existsSync(nodePath) && 38 + !fs.existsSync(hostPath) 39 + ) { 40 + continue; 41 + } 42 + 43 + const web = fs.existsSync(webPath) 44 + ? fs.readFileSync(webPath, "utf8") 45 + : undefined; 46 + 47 + let url: string | undefined = undefined; 48 + const urlPath = path.join(dir, constants.repoUrlFile); 49 + if (type == ExtensionLoadSource.Normal && fs.existsSync(urlPath)) { 50 + url = fs.readFileSync(urlPath, "utf8"); 51 + } 52 + 53 + ret.push({ 54 + id: manifest.id, 55 + manifest, 56 + source: { 57 + type, 58 + url 59 + }, 60 + scripts: { 61 + web, 62 + webPath: web != null ? webPath : undefined, 63 + nodePath: fs.existsSync(nodePath) ? nodePath : undefined, 64 + hostPath: fs.existsSync(hostPath) ? hostPath : undefined 65 + } 66 + }); 67 + } 68 + 69 + return ret; 70 + } 71 + 72 + function getExtensionsNative(): DetectedExtension[] { 73 + const config = readConfig(); 74 + const res = []; 75 + 76 + res.push( 77 + ...loadDetectedExtensions(getCoreExtensionsPath(), ExtensionLoadSource.Core) 78 + ); 79 + 80 + res.push( 81 + ...loadDetectedExtensions(getExtensionsPath(), ExtensionLoadSource.Normal) 82 + ); 83 + 84 + for (const devSearchPath of config.devSearchPaths ?? []) { 85 + res.push( 86 + ...loadDetectedExtensions(devSearchPath, ExtensionLoadSource.Developer) 87 + ); 88 + } 89 + 90 + return res; 91 + } 92 + 93 + export function getExtensions(): DetectedExtension[] { 94 + webPreload: { 95 + return moonlightNode.extensions; 96 + } 97 + 98 + nodePreload: { 99 + return getExtensionsNative(); 100 + } 101 + 102 + injector: { 103 + return getExtensionsNative(); 104 + } 105 + 106 + throw new Error("Called getExtensions() outside of node-preload/web-preload"); 107 + }
+229
packages/core/src/extension/loader.ts
··· 1 + import { 2 + ExtensionWebExports, 3 + DetectedExtension, 4 + ProcessedExtensions 5 + } from "@moonlight-mod/types"; 6 + import { readConfig } from "../config"; 7 + import Logger from "../util/logger"; 8 + import { getExtensions } from "../extension"; 9 + import { registerPatch, registerWebpackModule } from "../patch"; 10 + import calculateDependencies from "../util/dependency"; 11 + import { createEventEmitter } from "../util/event"; 12 + 13 + const logger = new Logger("core/extension/loader"); 14 + 15 + async function loadExt(ext: DetectedExtension) { 16 + webPreload: { 17 + if (ext.scripts.web != null) { 18 + const source = 19 + ext.scripts.web + "\n//# sourceURL=file:///" + ext.scripts.webPath; 20 + const fn = new Function("require", "module", "exports", source); 21 + 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 + ]); 30 + 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 + }); 40 + 41 + registerPatch({ ...newPatch, ext: ext.id, id: idx }); 42 + idx++; 43 + } 44 + } else { 45 + registerPatch({ ...patch, ext: ext.id, id: idx }); 46 + idx++; 47 + } 48 + } 49 + } 50 + 51 + if (exports.webpackModules != null) { 52 + for (const [name, wp] of Object.entries(exports.webpackModules)) { 53 + registerWebpackModule({ ...wp, ext: ext.id, id: name }); 54 + } 55 + } 56 + } 57 + } 58 + 59 + nodePreload: { 60 + if (ext.scripts.nodePath != null) { 61 + try { 62 + const module = require(ext.scripts.nodePath); 63 + moonlightNode.nativesCache[ext.id] = module; 64 + } catch (e) { 65 + logger.error(`Failed to load extension "${ext.id}"`, e); 66 + } 67 + } 68 + } 69 + 70 + injector: { 71 + if (ext.scripts.hostPath != null) { 72 + try { 73 + require(ext.scripts.hostPath); 74 + } catch (e) { 75 + logger.error(`Failed to load extension "${ext.id}"`, e); 76 + } 77 + } 78 + } 79 + } 80 + 81 + /* 82 + This function resolves extensions and loads them, split into a few stages: 83 + 84 + - Duplicate detection (removing multiple extensions with the same internal ID) 85 + - Dependency resolution (creating a dependency graph & detecting circular dependencies) 86 + - Failed dependency pruning 87 + - Implicit dependency resolution (enabling extensions that are dependencies of other extensions) 88 + - Loading all extensions 89 + 90 + Instead of constructing an order from the dependency graph and loading 91 + extensions synchronously, we load them in parallel asynchronously. Loading 92 + extensions fires an event on completion, which allows us to await the loading 93 + of another extension, resolving dependencies & load order effectively. 94 + */ 95 + export async function loadExtensions( 96 + exts: DetectedExtension[] 97 + ): Promise<ProcessedExtensions> { 98 + const items = exts 99 + .map((ext) => { 100 + return { 101 + id: ext.id, 102 + data: ext 103 + }; 104 + }) 105 + .sort((a, b) => a.id.localeCompare(b.id)); 106 + 107 + const [sorted, dependencyGraph] = calculateDependencies( 108 + items, 109 + 110 + function fetchDep(id) { 111 + return exts.find((x) => x.id === id) ?? null; 112 + }, 113 + 114 + function getDeps(item) { 115 + return item.data.manifest.dependencies ?? []; 116 + }, 117 + 118 + function getIncompatible(item) { 119 + return item.data.manifest.incompatible ?? []; 120 + } 121 + ); 122 + exts = sorted.map((x) => x.data); 123 + 124 + logger.debug( 125 + "Implicit dependency stage - extension list:", 126 + exts.map((x) => x.id) 127 + ); 128 + const config = readConfig(); 129 + const implicitlyEnabled: string[] = []; 130 + 131 + function isEnabledInConfig(ext: DetectedExtension) { 132 + if (implicitlyEnabled.includes(ext.id)) return true; 133 + 134 + const entry = config.extensions[ext.id]; 135 + if (entry == null) return false; 136 + 137 + if (entry === true) return true; 138 + if (typeof entry === "object" && entry.enabled === true) return true; 139 + 140 + return false; 141 + } 142 + 143 + function validateDeps(ext: DetectedExtension) { 144 + if (isEnabledInConfig(ext)) { 145 + const deps = dependencyGraph.get(ext.id)!; 146 + for (const dep of deps.values()) { 147 + validateDeps(exts.find((e) => e.id === dep)!); 148 + } 149 + } else { 150 + const dependsOnMe = Array.from(dependencyGraph.entries()).filter( 151 + ([, v]) => v?.has(ext.id) 152 + ); 153 + 154 + if (dependsOnMe.length > 0) { 155 + logger.debug("Implicitly enabling extension", ext.id); 156 + implicitlyEnabled.push(ext.id); 157 + } 158 + } 159 + } 160 + 161 + for (const ext of exts) validateDeps(ext); 162 + exts = exts.filter((e) => isEnabledInConfig(e)); 163 + 164 + return { 165 + extensions: exts, 166 + dependencyGraph 167 + }; 168 + } 169 + 170 + export async function loadProcessedExtensions({ 171 + extensions, 172 + dependencyGraph 173 + }: ProcessedExtensions) { 174 + const eventEmitter = createEventEmitter(); 175 + const finished: Set<string> = new Set(); 176 + 177 + logger.debug( 178 + "Load stage - extension list:", 179 + extensions.map((x) => x.id) 180 + ); 181 + 182 + async function loadExtWithDependencies(ext: DetectedExtension) { 183 + const deps = Array.from(dependencyGraph.get(ext.id)!); 184 + 185 + // Wait for the dependencies to finish 186 + const waitPromises = deps.map( 187 + (dep: string) => 188 + new Promise<void>((r) => { 189 + function cb(eventDep: string) { 190 + if (eventDep === dep) { 191 + done(); 192 + } 193 + } 194 + 195 + function done() { 196 + eventEmitter.removeEventListener("ext-ready", cb); 197 + r(); 198 + } 199 + 200 + eventEmitter.addEventListener("ext-ready", cb); 201 + if (finished.has(dep)) done(); 202 + }) 203 + ); 204 + 205 + if (waitPromises.length > 0) { 206 + logger.debug( 207 + `Waiting on ${waitPromises.length} dependencies for "${ext.id}"` 208 + ); 209 + await Promise.all(waitPromises); 210 + } 211 + 212 + logger.debug(`Loading "${ext.id}"`); 213 + await loadExt(ext); 214 + 215 + finished.add(ext.id); 216 + eventEmitter.dispatchEvent("ext-ready", ext.id); 217 + logger.debug(`Loaded "${ext.id}"`); 218 + } 219 + 220 + webPreload: { 221 + for (const ext of extensions) { 222 + moonlight.enabledExtensions.add(ext.id); 223 + } 224 + } 225 + 226 + logger.debug("Loading all extensions"); 227 + await Promise.all(extensions.map(loadExtWithDependencies)); 228 + logger.info(`Loaded ${extensions.length} extensions`); 229 + }
+333
packages/core/src/patch.ts
··· 1 + import { 2 + PatchReplace, 3 + PatchReplaceType, 4 + ExplicitExtensionDependency, 5 + IdentifiedPatch, 6 + IdentifiedWebpackModule, 7 + WebpackJsonp, 8 + WebpackJsonpEntry, 9 + WebpackModuleFunc 10 + } from "@moonlight-mod/types"; 11 + import Logger from "./util/logger"; 12 + import calculateDependencies, { Dependency } from "./util/dependency"; 13 + import WebpackRequire from "@moonlight-mod/types/discord/require"; 14 + 15 + const logger = new Logger("core/patch"); 16 + 17 + // Can't be Set because we need splice 18 + let patches: IdentifiedPatch[] = []; 19 + let webpackModules: Set<IdentifiedWebpackModule> = new Set(); 20 + 21 + export function registerPatch(patch: IdentifiedPatch) { 22 + patches.push(patch); 23 + } 24 + 25 + export function registerWebpackModule(wp: IdentifiedWebpackModule) { 26 + webpackModules.add(wp); 27 + } 28 + 29 + /* 30 + The patching system functions by matching a string or regex against the 31 + .toString()'d copy of a Webpack module. When a patch happens, we reconstruct 32 + the module with the patched source and replace it, wrapping it in the process. 33 + 34 + We keep track of what modules we've patched (and their original sources), both 35 + so we don't wrap them twice and so we can debug what extensions are patching 36 + what Webpack modules. 37 + */ 38 + const moduleCache: Record<string, string> = {}; 39 + const patched: Record<string, Array<string>> = {}; 40 + 41 + function patchModules(entry: WebpackJsonpEntry[1]) { 42 + for (const [id, func] of Object.entries(entry)) { 43 + let moduleString = moduleCache.hasOwnProperty(id) 44 + ? moduleCache[id] 45 + : func.toString().replace(/\n/g, ""); 46 + 47 + for (const patch of patches) { 48 + if (patch.prerequisite != null && !patch.prerequisite()) { 49 + continue; 50 + } 51 + 52 + if (patch.find instanceof RegExp && patch.find.global) { 53 + // Reset state because global regexes are stateful for some reason 54 + patch.find.lastIndex = 0; 55 + } 56 + 57 + // indexOf is faster than includes by 0.25% lmao 58 + const match = 59 + typeof patch.find === "string" 60 + ? moduleString.indexOf(patch.find) !== -1 61 + : patch.find.test(moduleString); 62 + 63 + // Global regexes apply to all modules 64 + const shouldRemove = 65 + typeof patch.find === "string" ? true : !patch.find.global; 66 + 67 + if (match) { 68 + moonlight.unpatched.delete(patch); 69 + 70 + // We ensured all arrays get turned into normal PatchReplace objects on register 71 + const replace = patch.replace as PatchReplace; 72 + 73 + if ( 74 + replace.type === undefined || 75 + replace.type == PatchReplaceType.Normal 76 + ) { 77 + // tsc fails to detect the overloads for this, so I'll just do this 78 + // Verbose, but it works 79 + let replaced; 80 + if (typeof replace.replacement === "string") { 81 + replaced = moduleString.replace(replace.match, replace.replacement); 82 + } else { 83 + replaced = moduleString.replace(replace.match, replace.replacement); 84 + } 85 + 86 + if (replaced === moduleString) { 87 + logger.warn("Patch replacement failed", id, patch); 88 + continue; 89 + } 90 + 91 + // Store what extensions patched what modules for easier debugging 92 + patched[id] = patched[id] || []; 93 + patched[id].push(`${patch.ext}#${patch.id}`); 94 + 95 + // Webpack module arguments are minified, so we replace them with consistent names 96 + // We have to wrap it so things don't break, though 97 + const patchedStr = patched[id].sort().join(", "); 98 + 99 + const wrapped = 100 + `(${replaced}).apply(this, arguments)\n` + 101 + `// Patched by moonlight: ${patchedStr}\n` + 102 + `//# sourceURL=Webpack-Module-${id}`; 103 + 104 + try { 105 + const func = new Function( 106 + "module", 107 + "exports", 108 + "require", 109 + wrapped 110 + ) as WebpackModuleFunc; 111 + entry[id] = func; 112 + entry[id].__moonlight = true; 113 + moduleString = replaced; 114 + } catch (e) { 115 + logger.warn("Error constructing function for patch", e); 116 + } 117 + } else if (replace.type == PatchReplaceType.Module) { 118 + // Directly replace the module with a new one 119 + const newModule = replace.replacement(moduleString); 120 + entry[id] = newModule; 121 + entry[id].__moonlight = true; 122 + moduleString = 123 + newModule.toString().replace(/\n/g, "") + 124 + `//# sourceURL=Webpack-Module-${id}`; 125 + } 126 + 127 + if (shouldRemove) { 128 + patches.splice( 129 + patches.findIndex((p) => p.ext === patch.ext && p.id === patch.id), 130 + 1 131 + ); 132 + } 133 + } 134 + } 135 + 136 + if (moonlightNode.config.patchAll === true) { 137 + if ( 138 + (typeof id !== "string" || !id.includes("_")) && 139 + !entry[id].__moonlight 140 + ) { 141 + const wrapped = 142 + `(${moduleString}).apply(this, arguments)\n` + 143 + `//# sourceURL=Webpack-Module-${id}`; 144 + entry[id] = new Function( 145 + "module", 146 + "exports", 147 + "require", 148 + wrapped 149 + ) as WebpackModuleFunc; 150 + entry[id].__moonlight = true; 151 + } 152 + } 153 + 154 + moduleCache[id] = moduleString; 155 + } 156 + } 157 + 158 + /* 159 + Similar to patching, we also want to inject our own custom Webpack modules 160 + into Discord's Webpack instance. We abuse pollution on the push function to 161 + mark when we've completed it already. 162 + */ 163 + let chunkId = Number.MAX_SAFE_INTEGER; 164 + 165 + function handleModuleDependencies() { 166 + const modules = Array.from(webpackModules.values()); 167 + 168 + const dependencies: Dependency<string, IdentifiedWebpackModule>[] = 169 + modules.map((wp) => { 170 + return { 171 + id: `${wp.ext}_${wp.id}`, 172 + data: wp 173 + }; 174 + }); 175 + 176 + const [sorted, _] = calculateDependencies( 177 + dependencies, 178 + 179 + function fetchDep(id) { 180 + return modules.find((x) => id === `${x.ext}_${x.id}`) ?? null; 181 + }, 182 + 183 + function getDeps(item) { 184 + const deps = item.data?.dependencies ?? []; 185 + return ( 186 + deps.filter( 187 + (dep) => !(dep instanceof RegExp || typeof dep === "string") 188 + ) as ExplicitExtensionDependency[] 189 + ).map((x) => `${x.ext}_${x.id}`); 190 + } 191 + ); 192 + 193 + webpackModules = new Set(sorted.map((x) => x.data)); 194 + } 195 + 196 + const injectedWpModules: IdentifiedWebpackModule[] = []; 197 + function injectModules(entry: WebpackJsonpEntry[1]) { 198 + const modules: Record<string, WebpackModuleFunc> = {}; 199 + const entrypoints: string[] = []; 200 + let inject = false; 201 + 202 + for (const [modId, mod] of Object.entries(entry)) { 203 + const modStr = mod.toString(); 204 + const wpModules = Array.from(webpackModules.values()); 205 + for (const wpModule of wpModules) { 206 + const id = wpModule.ext + "_" + wpModule.id; 207 + if (wpModule.dependencies) { 208 + const deps = new Set(wpModule.dependencies); 209 + 210 + // FIXME: This dependency resolution might fail if the things we want 211 + // got injected earlier. If weird dependencies fail, this is likely why. 212 + if (deps.size) { 213 + for (const dep of deps.values()) { 214 + if (typeof dep === "string") { 215 + if (modStr.includes(dep)) deps.delete(dep); 216 + } else if (dep instanceof RegExp) { 217 + if (dep.test(modStr)) deps.delete(dep); 218 + } else if ( 219 + injectedWpModules.find( 220 + (x) => x.ext === dep.ext && x.id === dep.id 221 + ) 222 + ) { 223 + deps.delete(dep); 224 + } 225 + } 226 + 227 + if (deps.size !== 0) { 228 + // Update the deps that have passed 229 + webpackModules.delete(wpModule); 230 + wpModule.dependencies = Array.from(deps); 231 + webpackModules.add(wpModule); 232 + continue; 233 + } 234 + 235 + wpModule.dependencies = Array.from(deps); 236 + } 237 + } 238 + 239 + webpackModules.delete(wpModule); 240 + injectedWpModules.push(wpModule); 241 + 242 + inject = true; 243 + 244 + modules[id] = wpModule.run; 245 + if (wpModule.entrypoint) entrypoints.push(id); 246 + } 247 + if (!webpackModules.size) break; 248 + } 249 + 250 + if (inject) { 251 + logger.debug("Injecting modules:", modules, entrypoints); 252 + window.webpackChunkdiscord_app.push([ 253 + [--chunkId], 254 + modules, 255 + (require: typeof WebpackRequire) => entrypoints.map(require) 256 + ]); 257 + } 258 + } 259 + 260 + declare global { 261 + interface Window { 262 + webpackChunkdiscord_app: WebpackJsonp; 263 + } 264 + } 265 + 266 + /* 267 + Webpack modules are bundled into an array of arrays that hold each function. 268 + Since we run code before Discord, we can create our own Webpack array and 269 + hijack the .push function on it. 270 + 271 + From there, we iterate over the object (mapping IDs to functions) and patch 272 + them accordingly. 273 + */ 274 + export async function installWebpackPatcher() { 275 + await handleModuleDependencies(); 276 + 277 + let realWebpackJsonp: WebpackJsonp | null = null; 278 + Object.defineProperty(window, "webpackChunkdiscord_app", { 279 + set: (jsonp: WebpackJsonp) => { 280 + // Don't let Sentry mess with Webpack 281 + const stack = new Error().stack!; 282 + if (stack.includes("sentry.")) return; 283 + 284 + realWebpackJsonp = jsonp; 285 + const realPush = jsonp.push; 286 + if (jsonp.push.__moonlight !== true) { 287 + jsonp.push = (items) => { 288 + patchModules(items[1]); 289 + 290 + try { 291 + const res = realPush.apply(realWebpackJsonp, [items]); 292 + if (!realPush.__moonlight) { 293 + logger.trace("Injecting Webpack modules", items[1]); 294 + injectModules(items[1]); 295 + } 296 + 297 + return res; 298 + } catch (err) { 299 + logger.error("Failed to inject Webpack modules:", err); 300 + return 0; 301 + } 302 + }; 303 + 304 + jsonp.push.bind = (thisArg: any, ...args: any[]) => { 305 + return realPush.bind(thisArg, ...args); 306 + }; 307 + 308 + jsonp.push.__moonlight = true; 309 + if (!realPush.__moonlight) { 310 + logger.debug("Injecting Webpack modules with empty entry"); 311 + // Inject an empty entry to cause iteration to happen once 312 + // Kind of a dirty hack but /shrug 313 + injectModules({ deez: () => {} }); 314 + } 315 + } 316 + }, 317 + 318 + get: () => { 319 + const stack = new Error().stack!; 320 + if (stack.includes("sentry.")) return []; 321 + return realWebpackJsonp; 322 + } 323 + }); 324 + 325 + registerWebpackModule({ 326 + ext: "moonlight", 327 + id: "fix_rspack_init_modules", 328 + entrypoint: true, 329 + run: function (module, exports, require) { 330 + patchModules(require.m); 331 + } 332 + }); 333 + }
packages/core/src/util/clone.ts

This is a binary file and will not be displayed.

+65
packages/core/src/util/data.ts
··· 1 + import { constants } from "@moonlight-mod/types"; 2 + import requireImport from "./import"; 3 + 4 + export function getMoonlightDir(): string { 5 + const { app, ipcRenderer } = require("electron"); 6 + const fs = requireImport("fs"); 7 + const path = requireImport("path"); 8 + 9 + let appData = ""; 10 + injector: { 11 + appData = app.getPath("appData"); 12 + } 13 + 14 + nodePreload: { 15 + appData = ipcRenderer.sendSync(constants.ipcGetAppData); 16 + } 17 + 18 + const dir = path.join(appData, "moonlight-mod"); 19 + if (!fs.existsSync(dir)) fs.mkdirSync(dir); 20 + 21 + return dir; 22 + } 23 + 24 + type BuildInfo = { 25 + releaseChannel: string; 26 + version: string; 27 + }; 28 + 29 + export function getConfigPath(): string { 30 + const dir = getMoonlightDir(); 31 + const fs = requireImport("fs"); 32 + const path = requireImport("path"); 33 + 34 + const buildInfoPath = path.join(process.resourcesPath, "build_info.json"); 35 + const buildInfo: BuildInfo = JSON.parse( 36 + fs.readFileSync(buildInfoPath, "utf8") 37 + ); 38 + 39 + const configPath = path.join(dir, buildInfo.releaseChannel + ".json"); 40 + return configPath; 41 + } 42 + 43 + function getPathFromMoonlight(...names: string[]): string { 44 + const dir = getMoonlightDir(); 45 + const fs = requireImport("fs"); 46 + const path = requireImport("path"); 47 + 48 + const target = path.join(dir, ...names); 49 + if (!fs.existsSync(target)) fs.mkdirSync(target); 50 + 51 + return target; 52 + } 53 + 54 + export function getExtensionsPath(): string { 55 + return getPathFromMoonlight(constants.extensionsDir); 56 + } 57 + 58 + export function getCoreExtensionsPath(): string { 59 + if (MOONLIGHT_PROD) { 60 + return getPathFromMoonlight(constants.distDir, constants.coreExtensionsDir); 61 + } else { 62 + const path = requireImport("path"); 63 + return path.join(__dirname, constants.coreExtensionsDir); 64 + } 65 + }
+121
packages/core/src/util/dependency.ts
··· 1 + import Logger from "./logger"; 2 + 3 + export type Dependency<T, D> = { 4 + id: T; 5 + data: D; 6 + }; 7 + 8 + const logger = new Logger("core/util/dependency"); 9 + 10 + export default function calculateDependencies<T, D>( 11 + origItems: Dependency<T, D>[], 12 + fetchDep: (id: T) => D | null, 13 + getDeps: (item: Dependency<T, D>) => T[], 14 + getIncompatible?: (item: Dependency<T, D>) => T[] 15 + ): [Dependency<T, D>[], Map<T, Set<T> | null>] { 16 + logger.trace("sortDependencies begin", origItems); 17 + let items = [...origItems]; 18 + 19 + if (getIncompatible != null) { 20 + for (const item of items) { 21 + const incompatibleItems = getIncompatible(item); 22 + for (const incompatibleItem of incompatibleItems) { 23 + if (items.find((x) => x.id === incompatibleItem) != null) { 24 + logger.warn( 25 + `Incompatible dependency detected: "${item.id}" and "${incompatibleItem}" - removing "${incompatibleItem}"` 26 + ); 27 + 28 + items = items.filter((x) => x.id !== incompatibleItem); 29 + } 30 + } 31 + } 32 + } 33 + 34 + const dependencyGraph = new Map<T, Set<T> | null>(); 35 + for (const item of items) { 36 + const fullDeps: Set<T> = new Set(); 37 + let failed = false; 38 + 39 + function resolveDeps(id: T, root: boolean) { 40 + if (id === item.id && !root) { 41 + logger.warn(`Circular dependency detected: "${item.id}"`); 42 + failed = true; 43 + return; 44 + } 45 + 46 + const obj = fetchDep(id); 47 + if (obj == null) { 48 + logger.warn(`Missing dependency detected`, id); 49 + failed = true; 50 + return; 51 + } 52 + 53 + if (!root) fullDeps.add(id); 54 + 55 + for (const dep of getDeps({ id, data: obj })) { 56 + resolveDeps(dep, false); 57 + } 58 + } 59 + 60 + resolveDeps(item.id, true); 61 + dependencyGraph.set(item.id, failed ? null : fullDeps); 62 + } 63 + 64 + logger.trace("Failed stage", items); 65 + function isFailed(id: T) { 66 + const deps = dependencyGraph.get(id); 67 + if (deps === null) return true; 68 + 69 + // For some reason this can be undefined. If it is, it's not null, so we 70 + // didn't explicitly fail. FIXME too tired to investigate 71 + if (deps === undefined) return false; 72 + 73 + for (const dep of deps) { 74 + if (isFailed(dep)) return true; 75 + } 76 + 77 + return false; 78 + } 79 + 80 + const failed = items.filter((item) => isFailed(item.id)); 81 + if (failed.length > 0) { 82 + logger.warn("Skipping failed items", failed); 83 + items = items.filter((item) => !failed.includes(item)); 84 + } 85 + 86 + logger.trace("Sorting stage", items); 87 + const sorted: Dependency<T, D>[] = []; 88 + 89 + // Clone the dependency graph to return it later 90 + const backupDependencyGraph = new Map(dependencyGraph); 91 + for (const item of items) { 92 + dependencyGraph.set(item.id, new Set(dependencyGraph.get(item.id))); 93 + } 94 + 95 + while ( 96 + Array.from(dependencyGraph.values()).filter((x) => x != null).length > 0 97 + ) { 98 + const noDependents = items.filter( 99 + (e) => dependencyGraph.get(e.id)?.size === 0 100 + ); 101 + 102 + if (noDependents.length === 0) { 103 + logger.warn("Stuck dependency graph detected", dependencyGraph); 104 + break; 105 + } 106 + 107 + for (const item of noDependents) { 108 + sorted.push(item); 109 + dependencyGraph.delete(item.id); 110 + } 111 + 112 + for (const deps of dependencyGraph.values()) { 113 + for (const item of noDependents) { 114 + deps?.delete(item.id); 115 + } 116 + } 117 + } 118 + 119 + logger.trace("sortDependencies end", sorted); 120 + return [sorted, backupDependencyGraph]; 121 + }
+79
packages/core/src/util/event.ts
··· 1 + export type MoonlightEventCallback = (data: string) => void; 2 + 3 + export interface MoonlightEventEmitter { 4 + dispatchEvent: (id: string, data: string) => void; 5 + addEventListener: (id: string, cb: MoonlightEventCallback) => void; 6 + removeEventListener: (id: string, cb: MoonlightEventCallback) => void; 7 + } 8 + 9 + function nodeMethod(): MoonlightEventEmitter { 10 + const EventEmitter = require("events"); 11 + const eventEmitter = new EventEmitter(); 12 + const listeners = new Map<MoonlightEventCallback, (...args: any[]) => void>(); 13 + 14 + return { 15 + dispatchEvent: (id: string, data: string) => { 16 + eventEmitter.emit(id, data); 17 + }, 18 + 19 + addEventListener: (id: string, cb: (data: string) => void) => { 20 + if (listeners.has(cb)) return; 21 + 22 + function listener(data: string) { 23 + cb(data); 24 + } 25 + 26 + listeners.set(cb, listener); 27 + eventEmitter.on(id, listener); 28 + }, 29 + 30 + removeEventListener: (id: string, cb: (data: string) => void) => { 31 + const listener = listeners.get(cb); 32 + if (listener == null) return; 33 + listeners.delete(cb); 34 + eventEmitter.off(id, listener); 35 + } 36 + }; 37 + } 38 + 39 + export function createEventEmitter(): MoonlightEventEmitter { 40 + webPreload: { 41 + const eventEmitter = new EventTarget(); 42 + const listeners = new Map<MoonlightEventCallback, (e: Event) => void>(); 43 + 44 + return { 45 + dispatchEvent: (id: string, data: string) => { 46 + eventEmitter.dispatchEvent(new CustomEvent(id, { detail: data })); 47 + }, 48 + 49 + addEventListener: (id: string, cb: (data: string) => void) => { 50 + if (listeners.has(cb)) return; 51 + 52 + function listener(e: Event) { 53 + const event = e as CustomEvent<string>; 54 + cb(event.detail); 55 + } 56 + 57 + listeners.set(cb, listener); 58 + eventEmitter.addEventListener(id, listener); 59 + }, 60 + 61 + removeEventListener: (id: string, cb: (data: string) => void) => { 62 + const listener = listeners.get(cb); 63 + if (listener == null) return; 64 + listeners.delete(cb); 65 + eventEmitter.removeEventListener(id, listener); 66 + } 67 + }; 68 + } 69 + 70 + nodePreload: { 71 + return nodeMethod(); 72 + } 73 + 74 + injector: { 75 + return nodeMethod(); 76 + } 77 + 78 + throw new Error("Called createEventEmitter() in an impossible environment"); 79 + }
+25
packages/core/src/util/import.ts
··· 1 + /* 2 + For tree shaking reasons, sometimes we need to require() instead of an import 3 + statement at the top of the module (like config, which runs node *and* web). 4 + 5 + require() doesn't seem to carry the types from @types/node, so this allows us 6 + to requireImport("fs") and still keep the types of fs. 7 + 8 + In the future, I'd like to automate ImportTypes, but I think the type is only 9 + cemented if import is passed a string literal. 10 + */ 11 + 12 + const canRequire = ["path", "fs", "glob"] as const; 13 + type CanRequire = (typeof canRequire)[number]; 14 + 15 + type ImportTypes = { 16 + path: typeof import("path"); 17 + fs: typeof import("fs"); 18 + glob: typeof import("glob"); 19 + }; 20 + 21 + export default function requireImport<T extends CanRequire>( 22 + type: T 23 + ): Awaited<ImportTypes[T]> { 24 + return require(type); 25 + }
+93
packages/core/src/util/logger.ts
··· 1 + import { LogLevel } from "@moonlight-mod/types/logger"; 2 + import { readConfig } from "../config"; 3 + 4 + const colors = { 5 + [LogLevel.SILLY]: "#EDD3E9", 6 + [LogLevel.TRACE]: "#000000", 7 + [LogLevel.DEBUG]: "#555555", 8 + [LogLevel.INFO]: "#8686d9", 9 + [LogLevel.WARN]: "#5454d1", 10 + [LogLevel.ERROR]: "#FF0000" 11 + }; 12 + 13 + const config = readConfig(); 14 + let maxLevel = LogLevel.INFO; 15 + if (config.loggerLevel != null) { 16 + const enumValue = 17 + LogLevel[config.loggerLevel.toUpperCase() as keyof typeof LogLevel]; 18 + if (enumValue != null) { 19 + maxLevel = enumValue; 20 + } 21 + } 22 + 23 + export default class Logger { 24 + private name: string; 25 + 26 + constructor(name: string) { 27 + this.name = name; 28 + } 29 + 30 + silly(...args: any[]) { 31 + this.log(LogLevel.SILLY, args); 32 + } 33 + 34 + trace(...args: any[]) { 35 + this.log(LogLevel.TRACE, args); 36 + } 37 + 38 + debug(...args: any[]) { 39 + this.log(LogLevel.DEBUG, args); 40 + } 41 + 42 + info(...args: any[]) { 43 + this.log(LogLevel.INFO, args); 44 + } 45 + 46 + warn(...args: any[]) { 47 + this.log(LogLevel.WARN, args); 48 + } 49 + 50 + error(...args: any[]) { 51 + this.log(LogLevel.ERROR, args); 52 + } 53 + 54 + log(level: LogLevel, obj: any[]) { 55 + let args = []; 56 + const logLevel = LogLevel[level].toUpperCase(); 57 + if (maxLevel > level) return; 58 + 59 + if (MOONLIGHT_WEB_PRELOAD) { 60 + args = [ 61 + `%c[${logLevel}]`, 62 + `background-color: ${colors[level]}; color: #FFFFFF;`, 63 + `[${this.name}]`, 64 + ...obj 65 + ]; 66 + } else { 67 + args = [`[${logLevel}]`, `[${this.name}]`, ...obj]; 68 + } 69 + 70 + switch (level) { 71 + case LogLevel.SILLY: 72 + case LogLevel.TRACE: 73 + console.trace(...args); 74 + break; 75 + 76 + case LogLevel.DEBUG: 77 + console.debug(...args); 78 + break; 79 + 80 + case LogLevel.INFO: 81 + console.info(...args); 82 + break; 83 + 84 + case LogLevel.WARN: 85 + console.warn(...args); 86 + break; 87 + 88 + case LogLevel.ERROR: 89 + console.error(...args); 90 + break; 91 + } 92 + } 93 + }
+3
packages/core/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json" 3 + }
+8
packages/injector/package.json
··· 1 + { 2 + "name": "@moonlight-mod/injector", 3 + "private": true, 4 + "dependencies": { 5 + "@moonlight-mod/types": "workspace:*", 6 + "@moonlight-mod/core": "workspace:*" 7 + } 8 + }
+164
packages/injector/src/index.ts
··· 1 + import electron, { 2 + BrowserWindowConstructorOptions, 3 + BrowserWindow as ElectronBrowserWindow, 4 + ipcMain, 5 + app, 6 + ipcRenderer 7 + } from "electron"; 8 + import Module from "module"; 9 + import { constants } from "@moonlight-mod/types"; 10 + import { readConfig } from "@moonlight-mod/core/config"; 11 + import { getExtensions } from "@moonlight-mod/core/extension"; 12 + import Logger from "@moonlight-mod/core/util/logger"; 13 + import { 14 + loadExtensions, 15 + loadProcessedExtensions 16 + } from "core/src/extension/loader"; 17 + import EventEmitter from "events"; 18 + 19 + let oldPreloadPath = ""; 20 + let corsAllow: string[] = []; 21 + 22 + ipcMain.on(constants.ipcGetOldPreloadPath, (e) => { 23 + e.returnValue = oldPreloadPath; 24 + }); 25 + ipcMain.on(constants.ipcGetAppData, (e) => { 26 + e.returnValue = app.getPath("appData"); 27 + }); 28 + ipcMain.handle(constants.ipcMessageBox, (_, opts) => { 29 + electron.dialog.showMessageBoxSync(opts); 30 + }); 31 + ipcMain.handle(constants.ipcSetCorsList, (_, list) => { 32 + corsAllow = list; 33 + }); 34 + 35 + function patchCsp(headers: Record<string, string[]>) { 36 + const directives = [ 37 + "style-src", 38 + "connect-src", 39 + "img-src", 40 + "font-src", 41 + "media-src", 42 + "worker-src", 43 + "prefetch-src" 44 + ]; 45 + const values = ["*", "blob:", "data:", "'unsafe-inline'", "disclip:"]; 46 + 47 + const csp = "content-security-policy"; 48 + if (headers[csp] == null) return; 49 + 50 + // This parsing is jank af lol 51 + const entries = headers[csp][0] 52 + .trim() 53 + .split(";") 54 + .map((x) => x.trim()) 55 + .filter((x) => x.length > 0) 56 + .map((x) => x.split(" ")) 57 + .map((x) => [x[0], x.slice(1)]); 58 + const parts = Object.fromEntries(entries); 59 + 60 + for (const directive of directives) { 61 + parts[directive] = values; 62 + } 63 + 64 + const stringified = Object.entries<string[]>(parts) 65 + .map(([key, value]) => { 66 + return `${key} ${value.join(" ")}`; 67 + }) 68 + .join("; "); 69 + headers[csp] = [stringified]; 70 + } 71 + 72 + class BrowserWindow extends ElectronBrowserWindow { 73 + constructor(opts: BrowserWindowConstructorOptions) { 74 + oldPreloadPath = opts.webPreferences!.preload!; 75 + opts.webPreferences!.preload = require.resolve("./node-preload.js"); 76 + 77 + super(opts); 78 + this.webContents.session.webRequest.onHeadersReceived((details, cb) => { 79 + if (details.responseHeaders != null) { 80 + if (details.resourceType == "mainFrame") { 81 + patchCsp(details.responseHeaders); 82 + } 83 + 84 + if (corsAllow.some((x) => details.url.startsWith(x))) { 85 + details.responseHeaders["access-control-allow-origin"] = ["*"]; 86 + } 87 + 88 + cb({ cancel: false, responseHeaders: details.responseHeaders }); 89 + } 90 + }); 91 + } 92 + } 93 + 94 + export async function inject(asarPath: string) { 95 + try { 96 + const config = readConfig(); 97 + const extensions = getExtensions(); 98 + 99 + // Duplicated in node-preload... oops 100 + function getConfig(ext: string) { 101 + const val = config.extensions[ext]; 102 + if (val == null || typeof val === "boolean") return undefined; 103 + return val.config; 104 + } 105 + 106 + global.moonlightHost = { 107 + asarPath, 108 + config, 109 + events: new EventEmitter(), 110 + extensions, 111 + processedExtensions: { 112 + extensions: [], 113 + dependencyGraph: new Map() 114 + }, 115 + 116 + getConfig, 117 + getConfigOption: <T>(ext: string, name: string) => { 118 + const config = getConfig(ext); 119 + if (config == null) return undefined; 120 + const option = config[name]; 121 + if (option == null) return undefined; 122 + return option as T; 123 + }, 124 + getLogger: (id: string) => { 125 + return new Logger(id); 126 + } 127 + }; 128 + 129 + patchElectron(); 130 + 131 + global.moonlightHost.processedExtensions = await loadExtensions(extensions); 132 + await loadProcessedExtensions(global.moonlightHost.processedExtensions); 133 + } catch (e) { 134 + console.error("Failed to inject", e); 135 + } 136 + 137 + require(asarPath); 138 + } 139 + 140 + function patchElectron() { 141 + const electronClone = {}; 142 + 143 + for (const property of Object.getOwnPropertyNames(electron)) { 144 + if (property === "BrowserWindow") { 145 + Object.defineProperty(electronClone, property, { 146 + get: () => BrowserWindow, 147 + enumerable: true, 148 + configurable: false 149 + }); 150 + } else { 151 + Object.defineProperty( 152 + electronClone, 153 + property, 154 + Object.getOwnPropertyDescriptor(electron, property)! 155 + ); 156 + } 157 + } 158 + 159 + // exports is a getter only on Windows, let's do some cursed shit instead 160 + const electronPath = require.resolve("electron"); 161 + const cachedElectron = require.cache[electronPath]!; 162 + require.cache[electronPath] = new Module(cachedElectron.id, require.main); 163 + require.cache[electronPath]!.exports = electronClone; 164 + }
+3
packages/injector/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json" 3 + }
+8
packages/node-preload/package.json
··· 1 + { 2 + "name": "@moonlight-mod/node-preload", 3 + "private": true, 4 + "dependencies": { 5 + "@moonlight-mod/core": "workspace:*", 6 + "@moonlight-mod/types": "workspace:*" 7 + } 8 + }
+93
packages/node-preload/src/index.ts
··· 1 + import { webFrame, ipcRenderer, contextBridge } from "electron"; 2 + import fs from "fs"; 3 + import path from "path"; 4 + 5 + import { readConfig, writeConfig } from "@moonlight-mod/core/config"; 6 + import { ProcessedExtensions, constants } from "@moonlight-mod/types"; 7 + import { getExtensions } from "@moonlight-mod/core/extension"; 8 + import { getExtensionsPath } from "@moonlight-mod/core/util/data"; 9 + import Logger from "@moonlight-mod/core/util/logger"; 10 + import { 11 + loadExtensions, 12 + loadProcessedExtensions 13 + } from "core/src/extension/loader"; 14 + 15 + async function injectGlobals() { 16 + const config = readConfig(); 17 + const extensions = getExtensions(); 18 + const processed = await loadExtensions(extensions); 19 + 20 + function getConfig(ext: string) { 21 + const val = config.extensions[ext]; 22 + if (val == null || typeof val === "boolean") return undefined; 23 + return val.config; 24 + } 25 + 26 + global.moonlightNode = { 27 + config, 28 + extensions: getExtensions(), 29 + processedExtensions: processed, 30 + nativesCache: {}, 31 + 32 + getConfig, 33 + getConfigOption: <T>(ext: string, name: string) => { 34 + const config = getConfig(ext); 35 + if (config == null) return undefined; 36 + const option = config[name]; 37 + if (option == null) return undefined; 38 + return option as T; 39 + }, 40 + getNatives: (ext: string) => global.moonlightNode.nativesCache[ext], 41 + getLogger: (id: string) => { 42 + return new Logger(id); 43 + }, 44 + 45 + getExtensionDir: (ext: string) => { 46 + const extPath = getExtensionsPath(); 47 + return path.join(extPath, ext); 48 + }, 49 + writeConfig 50 + }; 51 + 52 + await loadProcessedExtensions(processed); 53 + contextBridge.exposeInMainWorld("moonlightNode", moonlightNode); 54 + 55 + const extCors = moonlightNode.processedExtensions.extensions 56 + .map((x) => x.manifest.cors ?? []) 57 + .flat(); 58 + 59 + for (const repo of moonlightNode.config.repositories) { 60 + const url = new URL(repo); 61 + url.pathname = "/"; 62 + extCors.push(url.toString()); 63 + } 64 + 65 + ipcRenderer.invoke(constants.ipcSetCorsList, extCors); 66 + } 67 + 68 + async function loadPreload() { 69 + const webPreloadPath = path.join(__dirname, "web-preload.js"); 70 + const webPreload = fs.readFileSync(webPreloadPath, "utf8"); 71 + await webFrame.executeJavaScript(webPreload); 72 + } 73 + 74 + async function init(oldPreloadPath: string) { 75 + try { 76 + await injectGlobals(); 77 + await loadPreload(); 78 + } catch (e) { 79 + const message = e instanceof Error ? e.stack : e; 80 + await ipcRenderer.invoke(constants.ipcMessageBox, { 81 + title: "moonlight node-preload error", 82 + message: message 83 + }); 84 + } 85 + 86 + // Let Discord start even if we fail 87 + require(oldPreloadPath); 88 + } 89 + 90 + const oldPreloadPath: string = ipcRenderer.sendSync( 91 + constants.ipcGetOldPreloadPath 92 + ); 93 + init(oldPreloadPath);
+3
packages/node-preload/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json" 3 + }
+16
packages/types/package.json
··· 1 + { 2 + "name": "@moonlight-mod/types", 3 + "main": "./src/index.ts", 4 + "types": "./src/index.ts", 5 + "exports": { 6 + ".": "./src/index.ts", 7 + "./*": "./src/*.ts" 8 + }, 9 + "dependencies": { 10 + "@types/flux": "^3.1.12", 11 + "@types/node": "^20.6.2", 12 + "@types/react": "^18.2.22", 13 + "csstype": "^3.1.2", 14 + "standalone-electron-types": "^1.0.0" 15 + } 16 + }
+78
packages/types/src/config.ts
··· 1 + export type Config = { 2 + extensions: ConfigExtensions; 3 + repositories: string[]; 4 + devSearchPaths?: string[]; 5 + loggerLevel?: string; 6 + patchAll?: boolean; 7 + }; 8 + 9 + export type ConfigExtensions = 10 + | { [key: string]: boolean } 11 + | { [key: string]: ConfigExtension }; 12 + 13 + export type ConfigExtension = { 14 + enabled: boolean; 15 + config?: Record<string, any>; 16 + }; 17 + 18 + export enum ExtensionSettingType { 19 + Boolean = "boolean", 20 + Number = "number", 21 + String = "string", 22 + Select = "select", 23 + List = "list", 24 + Dictionary = "dictionary", 25 + Custom = "custom" 26 + } 27 + 28 + export type BooleanSettingType = { 29 + type: ExtensionSettingType.Boolean; 30 + default?: boolean; 31 + }; 32 + 33 + export type NumberSettingType = { 34 + type: ExtensionSettingType.Number; 35 + default?: number; 36 + min?: number; 37 + max?: number; 38 + }; 39 + 40 + export type StringSettingType = { 41 + type: ExtensionSettingType.String; 42 + default?: string; 43 + }; 44 + 45 + export type SelectSettingType = { 46 + type: ExtensionSettingType.Select; 47 + options: string[]; 48 + default?: string; 49 + }; 50 + 51 + export type ListSettingType = { 52 + type: ExtensionSettingType.List; 53 + options?: string[]; 54 + default?: string[]; 55 + }; 56 + 57 + export type DictionarySettingType = { 58 + type: ExtensionSettingType.Dictionary; 59 + default?: Record<string, string>; 60 + }; 61 + 62 + export type CustomSettingType = { 63 + type: ExtensionSettingType.Custom; 64 + default?: any; 65 + }; 66 + 67 + export type ExtensionSettingsManifest = { 68 + displayName?: string; 69 + description?: string; 70 + } & ( 71 + | BooleanSettingType 72 + | NumberSettingType 73 + | StringSettingType 74 + | SelectSettingType 75 + | ListSettingType 76 + | DictionarySettingType 77 + | CustomSettingType 78 + );
+9
packages/types/src/constants.ts
··· 1 + export const extensionsDir = "extensions"; 2 + export const distDir = "dist"; 3 + export const coreExtensionsDir = "core-extensions"; 4 + export const repoUrlFile = ".moonlight-repo-url"; 5 + 6 + export const ipcGetOldPreloadPath = "_moonlight_getOldPreloadPath"; 7 + export const ipcGetAppData = "_moonlight_getAppData"; 8 + export const ipcMessageBox = "_moonlight_messageBox"; 9 + export const ipcSetCorsList = "_moonlight_setCorsList";
+66
packages/types/src/coreExtensions.ts
··· 1 + import { FluxDefault, Store } from "./discord/common/Flux"; 2 + import WebpackRequire from "./discord/require"; 3 + import { WebpackModuleFunc } from "./discord/webpack"; 4 + import { CommonComponents as CommonComponents_ } from "./coreExtensions/components"; 5 + import { Dispatcher } from "flux"; 6 + import React from "react"; 7 + 8 + export type Spacepack = { 9 + inspect: (module: number | string) => WebpackModuleFunc | null; 10 + findByCode: (...args: (string | RegExp)[]) => any[]; 11 + findByExports: (...args: string[]) => any[]; 12 + require: typeof WebpackRequire; 13 + modules: Record<string, WebpackModuleFunc>; 14 + cache: Record<string, any>; 15 + findObjectFromKey: (exports: Record<string, any>, key: string) => any | null; 16 + findObjectFromValue: (exports: Record<string, any>, value: any) => any | null; 17 + findObjectFromKeyValuePair: ( 18 + exports: Record<string, any>, 19 + key: string, 20 + value: any 21 + ) => any | null; 22 + findFunctionByStrings: ( 23 + exports: Record<string, any>, 24 + ...strings: (string | RegExp)[] 25 + ) => Function | null; 26 + }; 27 + 28 + export type NoticeProps = { 29 + stores: Store<any>[]; 30 + element: React.FunctionComponent; 31 + }; 32 + 33 + export type SettingsSection = 34 + | { section: "DIVIDER"; pos: number } 35 + | { section: "HEADER"; label: string; pos: number } 36 + | { 37 + section: string; 38 + label: string; 39 + color: string | null; 40 + element: React.FunctionComponent; 41 + pos: number; 42 + notice?: NoticeProps; 43 + _moonlight_submenu?: () => any; 44 + }; 45 + 46 + export type Settings = { 47 + ourSections: SettingsSection[]; 48 + 49 + addSection: ( 50 + section: string, 51 + label: string, 52 + element: React.FunctionComponent, 53 + color?: string | null, 54 + pos?: number, 55 + notice?: NoticeProps 56 + ) => void; 57 + 58 + addDivider: (pos: number | null) => void; 59 + addHeader: (label: string, pos: number | null) => void; 60 + _mutateSections: (sections: SettingsSection[]) => SettingsSection[]; 61 + }; 62 + 63 + export type CommonReact = typeof import("react"); 64 + export type CommonFlux = FluxDefault; 65 + export type CommonComponents = CommonComponents_; // lol 66 + export type CommonFluxDispatcher = Dispatcher<any>;
+356
packages/types/src/coreExtensions/components.ts
··· 1 + import type { 2 + Component, 3 + Ref, 4 + PropsWithChildren, 5 + PropsWithoutRef, 6 + CSSProperties, 7 + ReactNode, 8 + MouseEvent, 9 + KeyboardEvent, 10 + ReactElement, 11 + ComponentClass, 12 + ComponentType, 13 + MouseEventHandler, 14 + KeyboardEventHandler 15 + } from "react"; 16 + import * as CSS from "csstype"; 17 + 18 + export enum TextInputSizes { 19 + DEFAULT = "inputDefault", 20 + MINI = "inputMini" 21 + } 22 + 23 + interface TextInput 24 + extends ComponentClass< 25 + PropsWithoutRef<{ 26 + value?: string; 27 + name?: string; 28 + className?: string; 29 + inputClassName?: string; 30 + inputPrefix?: string; 31 + disabled?: boolean; 32 + size?: TextInputSizes; 33 + editable?: boolean; 34 + inputRef?: Ref<any>; 35 + prefixElement?: Component; 36 + focusProps?: PropsWithoutRef<any>; 37 + error?: string; 38 + minLength?: number; 39 + maxLength?: number; 40 + onChange?: (value: string, name: string) => void; 41 + onFocus?: (event: any, name: string) => void; 42 + onBlur?: (event: any, name: string) => void; 43 + }> 44 + > { 45 + Sizes: TextInputSizes; 46 + } 47 + 48 + export enum FormTextTypes { 49 + DEFAULT = "default", 50 + DESCRIPTION = "description", 51 + ERROR = "error", 52 + INPUT_PLACEHOLDER = "placeholder", 53 + LABEL_BOLD = "labelBold", 54 + LABEL_DESCRIPTOR = "labelDescriptor", 55 + LABEL_SELECTED = "labelSelected", 56 + SUCCESS = "success" 57 + } 58 + 59 + interface FormText 60 + extends Component< 61 + PropsWithChildren<{ 62 + type?: FormTextTypes; 63 + className?: string; 64 + disabled?: boolean; 65 + selectable?: boolean; 66 + style?: CSSProperties; 67 + }> 68 + > { 69 + Types: FormTextTypes; 70 + } 71 + 72 + declare enum SliderMarkerPosition { 73 + ABOVE, 74 + BELOW 75 + } 76 + 77 + declare enum ButtonLooks { 78 + FILLED = "lookFilled", 79 + INVERTED = "lookInverted", 80 + OUTLINED = "lookOutlined", 81 + LINK = "lookLink", 82 + BLANK = "lookBlank" 83 + } 84 + declare enum ButtonColors { 85 + BRAND = "colorBrand", 86 + RED = "colorRed", 87 + GREEN = "colorGreen", 88 + YELLOW = "colorYellow", 89 + PRIMARY = "colorPrimary", 90 + LINK = "colorLink", 91 + WHITE = "colorWhite", 92 + BLACK = "colorBlack", 93 + TRANSPARENT = "colorTransparent", 94 + BRAND_NEW = "colorBrandNew", 95 + CUSTOM = "" 96 + } 97 + declare enum ButtonBorderColors { 98 + BRAND = "borderBrand", 99 + RED = "borderRed", 100 + GREEN = "borderGreen", 101 + YELLOW = "borderYellow", 102 + PRIMARY = "borderPrimary", 103 + LINK = "borderLink", 104 + WHITE = "borderWhite", 105 + BLACK = "borderBlack", 106 + TRANSPARENT = "borderTransparent", 107 + BRAND_NEW = "borderBrandNew" 108 + } 109 + declare enum ButtonHovers { 110 + DEFAULT = "", 111 + BRAND = "hoverBrand", 112 + RED = "hoverRed", 113 + GREEN = "hoverGreen", 114 + YELLOW = "hoverYellow", 115 + PRIMARY = "hoverPrimary", 116 + LINK = "hoverLink", 117 + WHITE = "hoverWhite", 118 + BLACK = "hoverBlack", 119 + TRANSPARENT = "hoverTransparent" 120 + } 121 + declare enum ButtonSizes { 122 + NONE = "", 123 + TINY = "sizeTiny", 124 + SMALL = "sizeSmall", 125 + MEDIUM = "sizeMedium", 126 + LARGE = "sizeLarge", 127 + XLARGE = "sizeXlarge", 128 + MIN = "sizeMin", 129 + MAX = "sizeMax", 130 + ICON = "sizeIcon" 131 + } 132 + 133 + type Button = ComponentType< 134 + PropsWithChildren<{ 135 + look?: ButtonLooks; 136 + color?: ButtonColors; 137 + borderColor?: ButtonBorderColors; 138 + hover?: ButtonHovers; 139 + size?: ButtonSizes; 140 + fullWidth?: boolean; 141 + grow?: boolean; 142 + disabled?: boolean; 143 + submitting?: boolean; 144 + type?: string; 145 + style?: CSSProperties; 146 + wrapperClassName?: string; 147 + className?: string; 148 + innerClassName?: string; 149 + onClick?: MouseEventHandler; 150 + onDoubleClick?: MouseEventHandler; 151 + onMouseDown?: MouseEventHandler; 152 + onMouseUp?: MouseEventHandler; 153 + onMouseEnter?: MouseEventHandler; 154 + onMouseLeave?: MouseEventHandler; 155 + onKeyDown?: KeyboardEventHandler; 156 + rel?: any; 157 + buttonRef?: Ref<any>; 158 + focusProps?: PropsWithChildren<any>; 159 + "aria-label"?: string; 160 + submittingStartedLabel?: string; 161 + submittingFinishedLabel?: string; 162 + }> 163 + > & { 164 + Looks: typeof ButtonLooks; 165 + Colors: typeof ButtonColors; 166 + BorderColors: typeof ButtonBorderColors; 167 + Hovers: typeof ButtonHovers; 168 + Sizes: typeof ButtonSizes; 169 + }; 170 + 171 + export enum FlexDirection { 172 + VERTICAL = "vertical", 173 + HORIZONTAL = "horizontal", 174 + HORIZONTAL_REVERSE = "horizontalReverse" 175 + } 176 + 177 + declare enum FlexAlign { 178 + START = "alignStart", 179 + END = "alignEnd", 180 + CENTER = "alignCenter", 181 + STRETCH = "alignStretch", 182 + BASELINE = "alignBaseline" 183 + } 184 + declare enum FlexJustify { 185 + START = "justifyStart", 186 + END = "justifyEnd", 187 + CENTER = "justifyCenter", 188 + BETWEEN = "justifyBetween", 189 + AROUND = "justifyAround" 190 + } 191 + declare enum FlexWrap { 192 + NO_WRAP = "noWrap", 193 + WRAP = "wrap", 194 + WRAP_REVERSE = "wrapReverse" 195 + } 196 + interface Flex 197 + extends ComponentClass< 198 + PropsWithChildren<{ 199 + className?: string; 200 + direction?: FlexDirection; 201 + justify?: FlexJustify; 202 + align?: FlexAlign; 203 + wrap?: FlexWrap; 204 + shrink?: CSS.Property.FlexShrink; 205 + grow?: CSS.Property.FlexGrow; 206 + basis?: CSS.Property.FlexBasis; 207 + style?: CSSProperties; 208 + }> 209 + > { 210 + Direction: typeof FlexDirection; 211 + Align: typeof FlexAlign; 212 + Justify: typeof FlexJustify; 213 + Wrap: typeof FlexWrap; 214 + Child: Component< 215 + PropsWithChildren<{ 216 + className?: string; 217 + shrink?: CSS.Property.FlexShrink; 218 + grow?: CSS.Property.FlexGrow; 219 + basis?: CSS.Property.FlexBasis; 220 + style?: CSSProperties; 221 + wrap?: boolean; 222 + }> 223 + >; 224 + } 225 + 226 + // TODO: wtaf is up with react types not working in jsx 227 + export type CommonComponents = { 228 + Clickable: Component< 229 + PropsWithChildren<{ 230 + onClick?: () => void; 231 + href?: any; 232 + onKeyPress?: () => void; 233 + ignoreKeyPress?: boolean; 234 + innerRef?: Ref<any>; 235 + focusProps?: any; 236 + tag?: string | Component; 237 + role?: any; 238 + tabIndex?: any; 239 + className?: string; 240 + }> 241 + >; 242 + TextInput: TextInput; 243 + Form: { 244 + Section: Component< 245 + PropsWithChildren<{ 246 + className?: string; 247 + titleClassName?: string; 248 + title?: ReactNode; 249 + icon?: ReactNode; 250 + disabled?: boolean; 251 + htmlFor?: any; 252 + tag?: string; 253 + }> 254 + >; 255 + Text: FormText; 256 + Title: Component< 257 + PropsWithChildren<{ 258 + tag?: string; 259 + className?: string; 260 + faded?: boolean; 261 + disabled?: boolean; 262 + required?: boolean; 263 + error?: string; 264 + }> 265 + >; 266 + }; 267 + Slider: ComponentClass< 268 + PropsWithChildren<{ 269 + disabled?: boolean; 270 + stickToMarkers?: boolean; 271 + className?: string; 272 + barStyles?: CSSProperties; 273 + fillStyles?: CSSProperties; 274 + mini?: boolean; 275 + hideBubble?: boolean; 276 + initialValue?: number; 277 + orientation?: "horizontal" | "vertical"; 278 + onValueRender?: (value: number) => string; 279 + renderMarker?: (marker: number) => ReactNode; 280 + getAriaValueText?: (value: number) => string; 281 + barClassName?: string; 282 + grabberClassName?: string; 283 + grabberStyles?: CSSProperties; 284 + markerPosition?: SliderMarkerPosition; 285 + "aria-hidden"?: "true" | "false"; 286 + "aria-label"?: string; 287 + "aria-labelledby"?: string; 288 + "aria-describedby"?: string; 289 + minValue?: number; 290 + maxValue?: number; 291 + asValueChanges?: (value: number) => void; 292 + onValueChange?: (value: number) => void; 293 + keyboardStep?: number; 294 + }> 295 + >; 296 + FormSwitch: ComponentClass<PropsWithChildren<any>>; 297 + Switch: ComponentClass<PropsWithChildren<any>>; 298 + Button: Button; 299 + SmallSlider: Component; 300 + Avatar: Component; 301 + Scroller: Component; 302 + Text: ComponentClass<PropsWithChildren<any>>; 303 + LegacyText: Component; 304 + Flex: Flex; 305 + Card: ComponentClass<PropsWithChildren<any>>; 306 + CardClasses: { 307 + card: string; 308 + cardHeader: string; 309 + }; 310 + ControlClasses: { 311 + container: string; 312 + control: string; 313 + disabled: string; 314 + dividerDefault: string; 315 + labelRow: string; 316 + note: string; 317 + title: string; 318 + titleDefault: string; 319 + titleMini: string; 320 + }; 321 + MarkdownParser: { 322 + parse: (text: string) => ReactElement; 323 + }; 324 + SettingsNotice: React.ComponentType<{ 325 + submitting: boolean; 326 + onReset: () => void; 327 + onSave: () => void; 328 + }>; 329 + TabBar: React.ComponentType<any> & { 330 + Item: React.ComponentType<any>; 331 + }; 332 + SingleSelect: React.ComponentType<{ 333 + autofocus?: boolean; 334 + clearable?: boolean; 335 + value?: string; 336 + options?: { 337 + value: string; 338 + label: string; 339 + }[]; 340 + onChange?: (value: string) => void; 341 + }>; 342 + Select: React.ComponentType<{ 343 + autofocus?: boolean; 344 + clearable?: boolean; 345 + value?: string[]; 346 + options?: { 347 + value: string; 348 + label: string; 349 + }[]; 350 + onChange?: (value: string[]) => void; 351 + }>; 352 + 353 + // TODO 354 + useVariableSelect: any; 355 + multiSelect: any; 356 + };
+57
packages/types/src/discord/common/Flux.ts
··· 1 + /* 2 + It seems like Discord maintains their own version of Flux that doesn't match 3 + the types on NPM. This is a heavy work in progress - if you encounter rough 4 + edges, please contribute! 5 + */ 6 + 7 + import { DependencyList } from "react"; 8 + import { Store as FluxStore } from "flux/utils"; 9 + import { Dispatcher as FluxDispatcher } from "flux"; 10 + import { ComponentConstructor } from "flux/lib/FluxContainer"; 11 + 12 + export declare abstract class Store<T> extends FluxStore<T> { 13 + static getAll: () => Store<any>[]; 14 + getName: () => string; 15 + emitChange: () => void; 16 + } 17 + 18 + interface ConnectStores { 19 + <T>( 20 + stores: Store<any>[], 21 + callback: T, 22 + context?: any 23 + ): ComponentConstructor<T>; 24 + } 25 + 26 + export type FluxDefault = { 27 + DeviceSettingsStore: any; // TODO 28 + Emitter: any; // @types/fbemitter 29 + OfflineCacheStore: any; // TODO 30 + PersistedStore: any; // TODO 31 + Store: typeof Store; 32 + Dispatcher: typeof FluxDispatcher; 33 + connectStores: ConnectStores; 34 + initialize: () => void; 35 + initialized: Promise<boolean>; 36 + destroy: () => void; 37 + useStateFromStores: UseStateFromStores; 38 + useStateFromStoresArray: UseStateFromStoresArray; 39 + useStateFromStoresObject: UseStateFromStoresObject; 40 + }; 41 + 42 + interface UseStateFromStores { 43 + <T>( 44 + stores: Store<any>[], 45 + callback: () => T, 46 + deps?: DependencyList, 47 + shouldUpdate?: (oldState: T, newState: T) => boolean 48 + ): T; 49 + } 50 + 51 + interface UseStateFromStoresArray { 52 + <T>(stores: Store<any>[], callback: () => T, deps?: DependencyList): T; 53 + } 54 + 55 + interface UseStateFromStoresObject { 56 + <T>(stores: Store<any>[], callback: () => T, deps?: DependencyList): T; 57 + }
+3
packages/types/src/discord/index.ts
··· 1 + export type Snowflake = `${number}`; 2 + 3 + export * from "./webpack";
+20
packages/types/src/discord/require.ts
··· 1 + import { 2 + Spacepack, 3 + CommonReact, 4 + CommonFlux, 5 + Settings, 6 + CommonComponents, 7 + CommonFluxDispatcher 8 + } from "../coreExtensions"; 9 + 10 + declare function WebpackRequire(id: string): any; 11 + declare function WebpackRequire(id: "spacepack_spacepack"): Spacepack; 12 + declare function WebpackRequire(id: "common_react"): CommonReact; 13 + declare function WebpackRequire(id: "common_flux"): CommonFlux; 14 + declare function WebpackRequire( 15 + id: "common_fluxDispatcher" 16 + ): CommonFluxDispatcher; 17 + declare function WebpackRequire(id: "settings_settings"): Settings; 18 + declare function WebpackRequire(id: "common_components"): CommonComponents; 19 + 20 + export default WebpackRequire;
+32
packages/types/src/discord/webpack.ts
··· 1 + import WebpackRequire from "./require"; 2 + 3 + export type WebpackRequireType = typeof WebpackRequire & { 4 + c: Record<string, WebpackModule>; 5 + m: Record<string, WebpackModuleFunc>; 6 + }; 7 + 8 + export type WebpackModule = { 9 + id: string | number; 10 + loaded: boolean; 11 + exports: any; 12 + }; 13 + 14 + export type WebpackModuleFunc = (( 15 + module: any, 16 + exports: any, 17 + require: WebpackRequireType 18 + ) => void) & { 19 + __moonlight?: boolean; 20 + }; 21 + 22 + export type WebpackJsonpEntry = [ 23 + number[], 24 + { [id: string]: WebpackModuleFunc }, 25 + (require: WebpackRequireType) => any 26 + ]; 27 + 28 + export type WebpackJsonp = WebpackJsonpEntry[] & { 29 + push: { 30 + __moonlight?: boolean; 31 + }; 32 + };
+125
packages/types/src/extension.ts
··· 1 + import { ExtensionSettingsManifest } from "./config"; 2 + import { Snowflake } from "./discord"; 3 + import { WebpackModuleFunc } from "./discord/webpack"; 4 + 5 + export enum ExtensionTag { 6 + Accessibility = "accessibility", 7 + Appearance = "appearance", 8 + Chat = "chat", 9 + Commands = "commands", 10 + ContextMenu = "contextMenu", 11 + DangerZone = "dangerZone", 12 + Development = "development", 13 + Fixes = "fixes", 14 + Fun = "fun", 15 + Markdown = "markdown", 16 + Voice = "voice", 17 + Privacy = "privacy", 18 + Profiles = "profiles", 19 + QualityOfLife = "qol", 20 + Library = "library" 21 + } 22 + 23 + export type ExtensionAuthor = 24 + | string 25 + | { 26 + name: string; 27 + id?: Snowflake; 28 + }; 29 + 30 + export type ExtensionManifest = { 31 + id: string; 32 + version?: string; 33 + 34 + meta?: { 35 + name?: string; 36 + tagline?: string; 37 + description?: string; 38 + authors?: ExtensionAuthor[]; 39 + deprecated?: boolean; 40 + tags?: ExtensionTag[]; 41 + source?: string; 42 + }; 43 + 44 + dependencies?: string[]; 45 + suggested?: string[]; 46 + incompatible?: string[]; // TODO: implement 47 + 48 + settings?: Record<string, ExtensionSettingsManifest>; 49 + cors?: string[]; 50 + }; 51 + 52 + export enum ExtensionLoadSource { 53 + Developer, 54 + Core, 55 + Normal 56 + } 57 + 58 + export type DetectedExtension = { 59 + id: string; 60 + manifest: ExtensionManifest; 61 + source: { type: ExtensionLoadSource; url?: string }; 62 + scripts: { 63 + web?: string; 64 + webPath?: string; 65 + nodePath?: string; 66 + hostPath?: string; 67 + }; 68 + }; 69 + 70 + export type ProcessedExtensions = { 71 + extensions: DetectedExtension[]; 72 + dependencyGraph: Map<string, Set<string> | null>; 73 + }; 74 + 75 + export type PatchMatch = string | RegExp; 76 + export type PatchReplaceFn = (substring: string, ...args: string[]) => string; 77 + export type PatchReplaceModule = (orig: string) => WebpackModuleFunc; 78 + 79 + export enum PatchReplaceType { 80 + Normal, 81 + Module 82 + } 83 + 84 + export type PatchReplace = 85 + | { 86 + type?: PatchReplaceType.Normal; 87 + match: PatchMatch; 88 + replacement: string | PatchReplaceFn; 89 + } 90 + | { 91 + type: PatchReplaceType.Module; 92 + replacement: PatchReplaceModule; 93 + }; 94 + 95 + export type Patch = { 96 + find: PatchMatch; 97 + replace: PatchReplace | PatchReplace[]; 98 + prerequisite?: () => boolean; 99 + }; 100 + 101 + export type ExplicitExtensionDependency = { 102 + ext: string; 103 + id: string; 104 + }; 105 + 106 + export type ExtensionDependency = string | RegExp | ExplicitExtensionDependency; 107 + 108 + export type ExtensionWebpackModule = { 109 + entrypoint?: boolean; 110 + dependencies?: ExtensionDependency[]; 111 + run: WebpackModuleFunc; 112 + }; 113 + 114 + export type ExtensionWebExports = { 115 + patches?: Patch[]; 116 + webpackModules?: Record<string, ExtensionWebpackModule>; 117 + }; 118 + 119 + export type IdentifiedPatch = Patch & { 120 + ext: string; 121 + id: number; 122 + }; 123 + 124 + export type IdentifiedWebpackModule = ExtensionWebpackModule & 125 + ExplicitExtensionDependency;
+51
packages/types/src/globals.ts
··· 1 + import { Logger } from "./logger"; 2 + import { Config, ConfigExtension } from "./config"; 3 + import { 4 + DetectedExtension, 5 + IdentifiedPatch, 6 + ProcessedExtensions 7 + } from "./extension"; 8 + import EventEmitter from "events"; 9 + 10 + export type MoonlightHost = { 11 + asarPath: string; 12 + config: Config; 13 + events: EventEmitter; 14 + extensions: DetectedExtension[]; 15 + processedExtensions: ProcessedExtensions; 16 + 17 + getConfig: (ext: string) => ConfigExtension["config"]; 18 + getConfigOption: <T>(ext: string, name: string) => T | undefined; 19 + getLogger: (id: string) => Logger; 20 + }; 21 + 22 + export type MoonlightNode = { 23 + config: Config; 24 + extensions: DetectedExtension[]; 25 + processedExtensions: ProcessedExtensions; 26 + nativesCache: Record<string, any>; 27 + 28 + getConfig: (ext: string) => ConfigExtension["config"]; 29 + getConfigOption: <T>(ext: string, name: string) => T | undefined; 30 + getNatives: (ext: string) => any | undefined; 31 + getLogger: (id: string) => Logger; 32 + 33 + getExtensionDir: (ext: string) => string; 34 + writeConfig: (config: Config) => void; 35 + }; 36 + 37 + export type MoonlightWeb = { 38 + unpatched: Set<IdentifiedPatch>; 39 + enabledExtensions: Set<string>; 40 + 41 + getConfig: (ext: string) => ConfigExtension["config"]; 42 + getConfigOption: <T>(ext: string, name: string) => T | undefined; 43 + getNatives: (ext: string) => any | undefined; 44 + getLogger: (id: string) => Logger; 45 + }; 46 + 47 + export enum MoonlightEnv { 48 + Injector = "injector", 49 + NodePreload = "node-preload", 50 + WebPreload = "web-preload" 51 + }
+30
packages/types/src/index.ts
··· 1 + /// <reference types="node" /> 2 + /// <reference types="standalone-electron-types" /> 3 + /// <reference types="react" /> 4 + /// <reference types="flux" /> 5 + 6 + import { 7 + MoonlightEnv, 8 + MoonlightHost, 9 + MoonlightNode, 10 + MoonlightWeb 11 + } from "./globals"; 12 + 13 + export * from "./discord"; 14 + export * from "./config"; 15 + export * from "./extension"; 16 + export * from "./globals"; 17 + export * from "./logger"; 18 + export * as constants from "./constants"; 19 + 20 + declare global { 21 + const MOONLIGHT_ENV: MoonlightEnv; 22 + const MOONLIGHT_PROD: boolean; 23 + const MOONLIGHT_INJECTOR: boolean; 24 + const MOONLIGHT_NODE_PRELOAD: boolean; 25 + const MOONLIGHT_WEB_PRELOAD: boolean; 26 + 27 + var moonlightHost: MoonlightHost; 28 + var moonlightNode: MoonlightNode; 29 + var moonlight: MoonlightWeb; 30 + }
+20
packages/types/src/logger.ts
··· 1 + export enum LogLevel { 2 + SILLY, 3 + TRACE, 4 + DEBUG, 5 + INFO, 6 + WARN, 7 + ERROR 8 + } 9 + 10 + type LogFunc = (...args: any[]) => void; 11 + 12 + export type Logger = { 13 + silly: LogFunc; 14 + trace: LogFunc; 15 + debug: LogFunc; 16 + info: LogFunc; 17 + warn: LogFunc; 18 + error: LogFunc; 19 + log: (level: LogLevel, args: any[]) => void; 20 + };
+8
packages/types/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "compilerOptions": { 4 + "outDir": "./dist", 5 + "declaration": true 6 + }, 7 + "include": ["./src/**/*", "src/index.ts"] 8 + }
+7
packages/web-preload/package.json
··· 1 + { 2 + "name": "@moonlight-mod/web-preload", 3 + "private": true, 4 + "dependencies": { 5 + "@moonlight-mod/core": "workspace:*" 6 + } 7 + }
+29
packages/web-preload/src/index.ts
··· 1 + import { 2 + loadExtensions, 3 + loadProcessedExtensions 4 + } from "@moonlight-mod/core/extension/loader"; 5 + import { installWebpackPatcher } from "@moonlight-mod/core/patch"; 6 + import Logger from "@moonlight-mod/core/util/logger"; 7 + 8 + (async () => { 9 + const logger = new Logger("web-preload"); 10 + 11 + window.moonlight = { 12 + unpatched: new Set(), 13 + enabledExtensions: new Set(), 14 + 15 + getConfig: moonlightNode.getConfig.bind(moonlightNode), 16 + getConfigOption: moonlightNode.getConfigOption.bind(moonlightNode), 17 + getNatives: moonlightNode.getNatives.bind(moonlightNode), 18 + getLogger: (id: string) => { 19 + return new Logger(id); 20 + } 21 + }; 22 + 23 + try { 24 + await loadProcessedExtensions(moonlightNode.processedExtensions); 25 + await installWebpackPatcher(); 26 + } catch (e) { 27 + logger.error("Error setting up web-preload", e); 28 + } 29 + })();
+3
packages/web-preload/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json" 3 + }
+654
pnpm-lock.yaml
··· 1 + lockfileVersion: '6.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + devDependencies: 11 + esbuild: 12 + specifier: ^0.19.3 13 + version: 0.19.3 14 + esbuild-copy-static-files: 15 + specifier: ^0.1.0 16 + version: 0.1.0 17 + 18 + packages/core: 19 + dependencies: 20 + '@moonlight-mod/types': 21 + specifier: workspace:* 22 + version: link:../types 23 + glob: 24 + specifier: ^10.3.4 25 + version: 10.3.4 26 + 27 + packages/core-extensions: 28 + dependencies: 29 + '@electron/asar': 30 + specifier: ^3.2.5 31 + version: 3.2.5 32 + '@moonlight-mod/types': 33 + specifier: workspace:* 34 + version: link:../types 35 + 36 + packages/injector: 37 + dependencies: 38 + '@moonlight-mod/core': 39 + specifier: workspace:* 40 + version: link:../core 41 + '@moonlight-mod/types': 42 + specifier: workspace:* 43 + version: link:../types 44 + 45 + packages/node-preload: 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 + 54 + packages/types: 55 + dependencies: 56 + '@types/flux': 57 + specifier: ^3.1.12 58 + version: 3.1.12 59 + '@types/node': 60 + specifier: ^20.6.2 61 + version: 20.6.2 62 + '@types/react': 63 + specifier: ^18.2.22 64 + version: 18.2.22 65 + csstype: 66 + specifier: ^3.1.2 67 + version: 3.1.2 68 + standalone-electron-types: 69 + specifier: ^1.0.0 70 + version: 1.0.0 71 + 72 + packages/web-preload: 73 + dependencies: 74 + '@moonlight-mod/core': 75 + specifier: workspace:* 76 + version: link:../core 77 + 78 + packages: 79 + 80 + /@electron/asar@3.2.5: 81 + resolution: {integrity: sha512-Ypahc2ElTj9YOrFvUHuoXv5Z/V1nPA5enlhmQapc578m/HZBHKTbqhoL5JZQjje2+/6Ti5AHh7Gj1/haeJa63Q==} 82 + engines: {node: '>=10.12.0'} 83 + hasBin: true 84 + dependencies: 85 + commander: 5.1.0 86 + glob: 7.2.3 87 + minimatch: 3.1.2 88 + dev: false 89 + 90 + /@esbuild/android-arm64@0.19.3: 91 + resolution: {integrity: sha512-w+Akc0vv5leog550kjJV9Ru+MXMR2VuMrui3C61mnysim0gkFCPOUTAfzTP0qX+HpN9Syu3YA3p1hf3EPqObRw==} 92 + engines: {node: '>=12'} 93 + cpu: [arm64] 94 + os: [android] 95 + requiresBuild: true 96 + dev: true 97 + optional: true 98 + 99 + /@esbuild/android-arm@0.19.3: 100 + resolution: {integrity: sha512-Lemgw4io4VZl9GHJmjiBGzQ7ONXRfRPHcUEerndjwiSkbxzrpq0Uggku5MxxrXdwJ+pTj1qyw4jwTu7hkPsgIA==} 101 + engines: {node: '>=12'} 102 + cpu: [arm] 103 + os: [android] 104 + requiresBuild: true 105 + dev: true 106 + optional: true 107 + 108 + /@esbuild/android-x64@0.19.3: 109 + resolution: {integrity: sha512-FKQJKkK5MXcBHoNZMDNUAg1+WcZlV/cuXrWCoGF/TvdRiYS4znA0m5Il5idUwfxrE20bG/vU1Cr5e1AD6IEIjQ==} 110 + engines: {node: '>=12'} 111 + cpu: [x64] 112 + os: [android] 113 + requiresBuild: true 114 + dev: true 115 + optional: true 116 + 117 + /@esbuild/darwin-arm64@0.19.3: 118 + resolution: {integrity: sha512-kw7e3FXU+VsJSSSl2nMKvACYlwtvZB8RUIeVShIEY6PVnuZ3c9+L9lWB2nWeeKWNNYDdtL19foCQ0ZyUL7nqGw==} 119 + engines: {node: '>=12'} 120 + cpu: [arm64] 121 + os: [darwin] 122 + requiresBuild: true 123 + dev: true 124 + optional: true 125 + 126 + /@esbuild/darwin-x64@0.19.3: 127 + resolution: {integrity: sha512-tPfZiwF9rO0jW6Jh9ipi58N5ZLoSjdxXeSrAYypy4psA2Yl1dAMhM71KxVfmjZhJmxRjSnb29YlRXXhh3GqzYw==} 128 + engines: {node: '>=12'} 129 + cpu: [x64] 130 + os: [darwin] 131 + requiresBuild: true 132 + dev: true 133 + optional: true 134 + 135 + /@esbuild/freebsd-arm64@0.19.3: 136 + resolution: {integrity: sha512-ERDyjOgYeKe0Vrlr1iLrqTByB026YLPzTytDTz1DRCYM+JI92Dw2dbpRHYmdqn6VBnQ9Bor6J8ZlNwdZdxjlSg==} 137 + engines: {node: '>=12'} 138 + cpu: [arm64] 139 + os: [freebsd] 140 + requiresBuild: true 141 + dev: true 142 + optional: true 143 + 144 + /@esbuild/freebsd-x64@0.19.3: 145 + resolution: {integrity: sha512-nXesBZ2Ad1qL+Rm3crN7NmEVJ5uvfLFPLJev3x1j3feCQXfAhoYrojC681RhpdOph8NsvKBBwpYZHR7W0ifTTA==} 146 + engines: {node: '>=12'} 147 + cpu: [x64] 148 + os: [freebsd] 149 + requiresBuild: true 150 + dev: true 151 + optional: true 152 + 153 + /@esbuild/linux-arm64@0.19.3: 154 + resolution: {integrity: sha512-qXvYKmXj8GcJgWq3aGvxL/JG1ZM3UR272SdPU4QSTzD0eymrM7leiZH77pvY3UetCy0k1xuXZ+VPvoJNdtrsWQ==} 155 + engines: {node: '>=12'} 156 + cpu: [arm64] 157 + os: [linux] 158 + requiresBuild: true 159 + dev: true 160 + optional: true 161 + 162 + /@esbuild/linux-arm@0.19.3: 163 + resolution: {integrity: sha512-zr48Cg/8zkzZCzDHNxXO/89bf9e+r4HtzNUPoz4GmgAkF1gFAFmfgOdCbR8zMbzFDGb1FqBBhdXUpcTQRYS1cQ==} 164 + engines: {node: '>=12'} 165 + cpu: [arm] 166 + os: [linux] 167 + requiresBuild: true 168 + dev: true 169 + optional: true 170 + 171 + /@esbuild/linux-ia32@0.19.3: 172 + resolution: {integrity: sha512-7XlCKCA0nWcbvYpusARWkFjRQNWNGlt45S+Q18UeS///K6Aw8bB2FKYe9mhVWy/XLShvCweOLZPrnMswIaDXQA==} 173 + engines: {node: '>=12'} 174 + cpu: [ia32] 175 + os: [linux] 176 + requiresBuild: true 177 + dev: true 178 + optional: true 179 + 180 + /@esbuild/linux-loong64@0.19.3: 181 + resolution: {integrity: sha512-qGTgjweER5xqweiWtUIDl9OKz338EQqCwbS9c2Bh5jgEH19xQ1yhgGPNesugmDFq+UUSDtWgZ264st26b3de8A==} 182 + engines: {node: '>=12'} 183 + cpu: [loong64] 184 + os: [linux] 185 + requiresBuild: true 186 + dev: true 187 + optional: true 188 + 189 + /@esbuild/linux-mips64el@0.19.3: 190 + resolution: {integrity: sha512-gy1bFskwEyxVMFRNYSvBauDIWNggD6pyxUksc0MV9UOBD138dKTzr8XnM2R4mBsHwVzeuIH8X5JhmNs2Pzrx+A==} 191 + engines: {node: '>=12'} 192 + cpu: [mips64el] 193 + os: [linux] 194 + requiresBuild: true 195 + dev: true 196 + optional: true 197 + 198 + /@esbuild/linux-ppc64@0.19.3: 199 + resolution: {integrity: sha512-UrYLFu62x1MmmIe85rpR3qou92wB9lEXluwMB/STDzPF9k8mi/9UvNsG07Tt9AqwPQXluMQ6bZbTzYt01+Ue5g==} 200 + engines: {node: '>=12'} 201 + cpu: [ppc64] 202 + os: [linux] 203 + requiresBuild: true 204 + dev: true 205 + optional: true 206 + 207 + /@esbuild/linux-riscv64@0.19.3: 208 + resolution: {integrity: sha512-9E73TfyMCbE+1AwFOg3glnzZ5fBAFK4aawssvuMgCRqCYzE0ylVxxzjEfut8xjmKkR320BEoMui4o/t9KA96gA==} 209 + engines: {node: '>=12'} 210 + cpu: [riscv64] 211 + os: [linux] 212 + requiresBuild: true 213 + dev: true 214 + optional: true 215 + 216 + /@esbuild/linux-s390x@0.19.3: 217 + resolution: {integrity: sha512-LlmsbuBdm1/D66TJ3HW6URY8wO6IlYHf+ChOUz8SUAjVTuaisfuwCOAgcxo3Zsu3BZGxmI7yt//yGOxV+lHcEA==} 218 + engines: {node: '>=12'} 219 + cpu: [s390x] 220 + os: [linux] 221 + requiresBuild: true 222 + dev: true 223 + optional: true 224 + 225 + /@esbuild/linux-x64@0.19.3: 226 + resolution: {integrity: sha512-ogV0+GwEmvwg/8ZbsyfkYGaLACBQWDvO0Kkh8LKBGKj9Ru8VM39zssrnu9Sxn1wbapA2qNS6BiLdwJZGouyCwQ==} 227 + engines: {node: '>=12'} 228 + cpu: [x64] 229 + os: [linux] 230 + requiresBuild: true 231 + dev: true 232 + optional: true 233 + 234 + /@esbuild/netbsd-x64@0.19.3: 235 + resolution: {integrity: sha512-o1jLNe4uzQv2DKXMlmEzf66Wd8MoIhLNO2nlQBHLtWyh2MitDG7sMpfCO3NTcoTMuqHjfufgUQDFRI5C+xsXQw==} 236 + engines: {node: '>=12'} 237 + cpu: [x64] 238 + os: [netbsd] 239 + requiresBuild: true 240 + dev: true 241 + optional: true 242 + 243 + /@esbuild/openbsd-x64@0.19.3: 244 + resolution: {integrity: sha512-AZJCnr5CZgZOdhouLcfRdnk9Zv6HbaBxjcyhq0StNcvAdVZJSKIdOiPB9az2zc06ywl0ePYJz60CjdKsQacp5Q==} 245 + engines: {node: '>=12'} 246 + cpu: [x64] 247 + os: [openbsd] 248 + requiresBuild: true 249 + dev: true 250 + optional: true 251 + 252 + /@esbuild/sunos-x64@0.19.3: 253 + resolution: {integrity: sha512-Acsujgeqg9InR4glTRvLKGZ+1HMtDm94ehTIHKhJjFpgVzZG9/pIcWW/HA/DoMfEyXmANLDuDZ2sNrWcjq1lxw==} 254 + engines: {node: '>=12'} 255 + cpu: [x64] 256 + os: [sunos] 257 + requiresBuild: true 258 + dev: true 259 + optional: true 260 + 261 + /@esbuild/win32-arm64@0.19.3: 262 + resolution: {integrity: sha512-FSrAfjVVy7TifFgYgliiJOyYynhQmqgPj15pzLyJk8BUsnlWNwP/IAy6GAiB1LqtoivowRgidZsfpoYLZH586A==} 263 + engines: {node: '>=12'} 264 + cpu: [arm64] 265 + os: [win32] 266 + requiresBuild: true 267 + dev: true 268 + optional: true 269 + 270 + /@esbuild/win32-ia32@0.19.3: 271 + resolution: {integrity: sha512-xTScXYi12xLOWZ/sc5RBmMN99BcXp/eEf7scUC0oeiRoiT5Vvo9AycuqCp+xdpDyAU+LkrCqEpUS9fCSZF8J3Q==} 272 + engines: {node: '>=12'} 273 + cpu: [ia32] 274 + os: [win32] 275 + requiresBuild: true 276 + dev: true 277 + optional: true 278 + 279 + /@esbuild/win32-x64@0.19.3: 280 + resolution: {integrity: sha512-FbUN+0ZRXsypPyWE2IwIkVjDkDnJoMJARWOcFZn4KPPli+QnKqF0z1anvfaYe3ev5HFCpRDLLBDHyOALLppWHw==} 281 + engines: {node: '>=12'} 282 + cpu: [x64] 283 + os: [win32] 284 + requiresBuild: true 285 + dev: true 286 + optional: true 287 + 288 + /@isaacs/cliui@8.0.2: 289 + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 290 + engines: {node: '>=12'} 291 + dependencies: 292 + string-width: 5.1.2 293 + string-width-cjs: /string-width@4.2.3 294 + strip-ansi: 7.1.0 295 + strip-ansi-cjs: /strip-ansi@6.0.1 296 + wrap-ansi: 8.1.0 297 + wrap-ansi-cjs: /wrap-ansi@7.0.0 298 + dev: false 299 + 300 + /@pkgjs/parseargs@0.11.0: 301 + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 302 + engines: {node: '>=14'} 303 + requiresBuild: true 304 + dev: false 305 + optional: true 306 + 307 + /@types/fbemitter@2.0.33: 308 + resolution: {integrity: sha512-KcSilwdl0D8YgXGL6l9d+rTBm2W7pDyTZrDEw0+IzqQ724676KJtMeO+xHodJewKFWZT+GFWaJubA5mpMxSkcg==} 309 + dev: false 310 + 311 + /@types/flux@3.1.12: 312 + resolution: {integrity: sha512-HZ8o/DTVNgcgnXoDyn0ZnjqEZMT4Chr4w5ktMQSbQAnqVDklasmRqNGd2agZDsk5i0jYHQLgQQuM782bWG7fUA==} 313 + dependencies: 314 + '@types/fbemitter': 2.0.33 315 + '@types/react': 18.2.22 316 + dev: false 317 + 318 + /@types/node@18.17.17: 319 + resolution: {integrity: sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==} 320 + dev: false 321 + 322 + /@types/node@20.6.2: 323 + resolution: {integrity: sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==} 324 + dev: false 325 + 326 + /@types/prop-types@15.7.6: 327 + resolution: {integrity: sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg==} 328 + dev: false 329 + 330 + /@types/react@18.2.22: 331 + resolution: {integrity: sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==} 332 + dependencies: 333 + '@types/prop-types': 15.7.6 334 + '@types/scheduler': 0.16.3 335 + csstype: 3.1.2 336 + dev: false 337 + 338 + /@types/scheduler@0.16.3: 339 + resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} 340 + dev: false 341 + 342 + /ansi-regex@5.0.1: 343 + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 344 + engines: {node: '>=8'} 345 + dev: false 346 + 347 + /ansi-regex@6.0.1: 348 + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 349 + engines: {node: '>=12'} 350 + dev: false 351 + 352 + /ansi-styles@4.3.0: 353 + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 354 + engines: {node: '>=8'} 355 + dependencies: 356 + color-convert: 2.0.1 357 + dev: false 358 + 359 + /ansi-styles@6.2.1: 360 + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 361 + engines: {node: '>=12'} 362 + dev: false 363 + 364 + /balanced-match@1.0.2: 365 + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 366 + dev: false 367 + 368 + /brace-expansion@1.1.11: 369 + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 370 + dependencies: 371 + balanced-match: 1.0.2 372 + concat-map: 0.0.1 373 + dev: false 374 + 375 + /brace-expansion@2.0.1: 376 + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 377 + dependencies: 378 + balanced-match: 1.0.2 379 + dev: false 380 + 381 + /color-convert@2.0.1: 382 + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 383 + engines: {node: '>=7.0.0'} 384 + dependencies: 385 + color-name: 1.1.4 386 + dev: false 387 + 388 + /color-name@1.1.4: 389 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 390 + dev: false 391 + 392 + /commander@5.1.0: 393 + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} 394 + engines: {node: '>= 6'} 395 + dev: false 396 + 397 + /concat-map@0.0.1: 398 + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} 399 + dev: false 400 + 401 + /cross-spawn@7.0.3: 402 + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 403 + engines: {node: '>= 8'} 404 + dependencies: 405 + path-key: 3.1.1 406 + shebang-command: 2.0.0 407 + which: 2.0.2 408 + dev: false 409 + 410 + /csstype@3.1.2: 411 + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 412 + dev: false 413 + 414 + /eastasianwidth@0.2.0: 415 + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 416 + dev: false 417 + 418 + /emoji-regex@8.0.0: 419 + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 420 + dev: false 421 + 422 + /emoji-regex@9.2.2: 423 + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 424 + dev: false 425 + 426 + /esbuild-copy-static-files@0.1.0: 427 + resolution: {integrity: sha512-KlpmYqANA1t2nZavEdItfcOjJC6wbHA21v35HJWN32DddGTWKNNGDKljUzbCPojmpD+wAw8/DXr5abJ4jFCE0w==} 428 + dev: true 429 + 430 + /esbuild@0.19.3: 431 + resolution: {integrity: sha512-UlJ1qUUA2jL2nNib1JTSkifQTcYTroFqRjwCFW4QYEKEsixXD5Tik9xML7zh2gTxkYTBKGHNH9y7txMwVyPbjw==} 432 + engines: {node: '>=12'} 433 + hasBin: true 434 + requiresBuild: true 435 + optionalDependencies: 436 + '@esbuild/android-arm': 0.19.3 437 + '@esbuild/android-arm64': 0.19.3 438 + '@esbuild/android-x64': 0.19.3 439 + '@esbuild/darwin-arm64': 0.19.3 440 + '@esbuild/darwin-x64': 0.19.3 441 + '@esbuild/freebsd-arm64': 0.19.3 442 + '@esbuild/freebsd-x64': 0.19.3 443 + '@esbuild/linux-arm': 0.19.3 444 + '@esbuild/linux-arm64': 0.19.3 445 + '@esbuild/linux-ia32': 0.19.3 446 + '@esbuild/linux-loong64': 0.19.3 447 + '@esbuild/linux-mips64el': 0.19.3 448 + '@esbuild/linux-ppc64': 0.19.3 449 + '@esbuild/linux-riscv64': 0.19.3 450 + '@esbuild/linux-s390x': 0.19.3 451 + '@esbuild/linux-x64': 0.19.3 452 + '@esbuild/netbsd-x64': 0.19.3 453 + '@esbuild/openbsd-x64': 0.19.3 454 + '@esbuild/sunos-x64': 0.19.3 455 + '@esbuild/win32-arm64': 0.19.3 456 + '@esbuild/win32-ia32': 0.19.3 457 + '@esbuild/win32-x64': 0.19.3 458 + dev: true 459 + 460 + /foreground-child@3.1.1: 461 + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} 462 + engines: {node: '>=14'} 463 + dependencies: 464 + cross-spawn: 7.0.3 465 + signal-exit: 4.1.0 466 + dev: false 467 + 468 + /fs.realpath@1.0.0: 469 + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 470 + dev: false 471 + 472 + /glob@10.3.4: 473 + resolution: {integrity: sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==} 474 + engines: {node: '>=16 || 14 >=14.17'} 475 + hasBin: true 476 + dependencies: 477 + foreground-child: 3.1.1 478 + jackspeak: 2.3.3 479 + minimatch: 9.0.3 480 + minipass: 7.0.3 481 + path-scurry: 1.10.1 482 + dev: false 483 + 484 + /glob@7.2.3: 485 + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 486 + dependencies: 487 + fs.realpath: 1.0.0 488 + inflight: 1.0.6 489 + inherits: 2.0.4 490 + minimatch: 3.1.2 491 + once: 1.4.0 492 + path-is-absolute: 1.0.1 493 + dev: false 494 + 495 + /inflight@1.0.6: 496 + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 497 + dependencies: 498 + once: 1.4.0 499 + wrappy: 1.0.2 500 + dev: false 501 + 502 + /inherits@2.0.4: 503 + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 504 + dev: false 505 + 506 + /is-fullwidth-code-point@3.0.0: 507 + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 508 + engines: {node: '>=8'} 509 + dev: false 510 + 511 + /isexe@2.0.0: 512 + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 513 + dev: false 514 + 515 + /jackspeak@2.3.3: 516 + resolution: {integrity: sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==} 517 + engines: {node: '>=14'} 518 + dependencies: 519 + '@isaacs/cliui': 8.0.2 520 + optionalDependencies: 521 + '@pkgjs/parseargs': 0.11.0 522 + dev: false 523 + 524 + /lru-cache@10.0.1: 525 + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} 526 + engines: {node: 14 || >=16.14} 527 + dev: false 528 + 529 + /minimatch@3.1.2: 530 + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 531 + dependencies: 532 + brace-expansion: 1.1.11 533 + dev: false 534 + 535 + /minimatch@9.0.3: 536 + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} 537 + engines: {node: '>=16 || 14 >=14.17'} 538 + dependencies: 539 + brace-expansion: 2.0.1 540 + dev: false 541 + 542 + /minipass@7.0.3: 543 + resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} 544 + engines: {node: '>=16 || 14 >=14.17'} 545 + dev: false 546 + 547 + /once@1.4.0: 548 + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 549 + dependencies: 550 + wrappy: 1.0.2 551 + dev: false 552 + 553 + /path-is-absolute@1.0.1: 554 + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 555 + engines: {node: '>=0.10.0'} 556 + dev: false 557 + 558 + /path-key@3.1.1: 559 + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 560 + engines: {node: '>=8'} 561 + dev: false 562 + 563 + /path-scurry@1.10.1: 564 + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} 565 + engines: {node: '>=16 || 14 >=14.17'} 566 + dependencies: 567 + lru-cache: 10.0.1 568 + minipass: 7.0.3 569 + dev: false 570 + 571 + /shebang-command@2.0.0: 572 + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 573 + engines: {node: '>=8'} 574 + dependencies: 575 + shebang-regex: 3.0.0 576 + dev: false 577 + 578 + /shebang-regex@3.0.0: 579 + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 580 + engines: {node: '>=8'} 581 + dev: false 582 + 583 + /signal-exit@4.1.0: 584 + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 585 + engines: {node: '>=14'} 586 + dev: false 587 + 588 + /standalone-electron-types@1.0.0: 589 + resolution: {integrity: sha512-0HOi/tlTz3mjWhsAz4uRbpQcHMZ+ifj1JzWW9nugykOHClBBG77ps8QinrzX1eow4Iw2pnC+RFaSYRgufF4BOg==} 590 + dependencies: 591 + '@types/node': 18.17.17 592 + dev: false 593 + 594 + /string-width@4.2.3: 595 + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 596 + engines: {node: '>=8'} 597 + dependencies: 598 + emoji-regex: 8.0.0 599 + is-fullwidth-code-point: 3.0.0 600 + strip-ansi: 6.0.1 601 + dev: false 602 + 603 + /string-width@5.1.2: 604 + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 605 + engines: {node: '>=12'} 606 + dependencies: 607 + eastasianwidth: 0.2.0 608 + emoji-regex: 9.2.2 609 + strip-ansi: 7.1.0 610 + dev: false 611 + 612 + /strip-ansi@6.0.1: 613 + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 614 + engines: {node: '>=8'} 615 + dependencies: 616 + ansi-regex: 5.0.1 617 + dev: false 618 + 619 + /strip-ansi@7.1.0: 620 + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 621 + engines: {node: '>=12'} 622 + dependencies: 623 + ansi-regex: 6.0.1 624 + dev: false 625 + 626 + /which@2.0.2: 627 + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 628 + engines: {node: '>= 8'} 629 + hasBin: true 630 + dependencies: 631 + isexe: 2.0.0 632 + dev: false 633 + 634 + /wrap-ansi@7.0.0: 635 + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 636 + engines: {node: '>=10'} 637 + dependencies: 638 + ansi-styles: 4.3.0 639 + string-width: 4.2.3 640 + strip-ansi: 6.0.1 641 + dev: false 642 + 643 + /wrap-ansi@8.1.0: 644 + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 645 + engines: {node: '>=12'} 646 + dependencies: 647 + ansi-styles: 6.2.1 648 + string-width: 5.1.2 649 + strip-ansi: 7.1.0 650 + dev: false 651 + 652 + /wrappy@1.0.2: 653 + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 654 + dev: false
+2
pnpm-workspace.yaml
··· 1 + packages: 2 + - "packages/*"
+18
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "es2016", 4 + "module": "es6", 5 + "esModuleInterop": true, 6 + "forceConsistentCasingInFileNames": true, 7 + "strict": true, 8 + "skipLibCheck": true, 9 + "moduleResolution": "bundler", 10 + "baseUrl": "./packages/", 11 + "jsx": "react", 12 + 13 + // disable unreachable code detection because it breaks with esbuild labels 14 + "allowUnreachableCode": true 15 + }, 16 + "include": ["./packages/**/*", "./env.d.ts"], 17 + "exclude": ["node_modules"] 18 + }