this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

1.2.0

authored by

Cynthia Foxwell and committed by
GitHub
c127fcec 7926793e

+3769 -760
+19 -1
.eslintrc.json
··· 1 1 { 2 + "root": true, 2 3 "extends": [ 3 4 "eslint:recommended", 4 5 "plugin:@typescript-eslint/recommended", ··· 47 48 "@typescript-eslint/no-var-requires": "off", 48 49 49 50 // https://canary.discord.com/channels/1154257010532032512/1154275441788583996/1181760413231230976 50 - "no-unused-labels": "off" 51 + "no-unused-labels": "off", 52 + 53 + // baseUrl being set to ./packages/ makes language server suggest "types/src" instead of "@moonlight-mod/types" 54 + "no-restricted-imports": [ 55 + "error", 56 + { 57 + "patterns": [ 58 + { 59 + "group": ["types/*"], 60 + "message": "Use @moonlight-mod/types instead" 61 + }, 62 + { 63 + "group": ["core/*"], 64 + "message": "Use @moonlight-mod/core instead" 65 + } 66 + ] 67 + } 68 + ] 51 69 }, 52 70 "settings": { 53 71 "react": {
+45
.github/workflows/browser.yml
··· 1 + name: Browser extension builds 2 + 3 + on: 4 + push: 5 + branches: 6 + - develop 7 + 8 + jobs: 9 + browser: 10 + name: Browser extension builds 11 + runs-on: ubuntu-latest 12 + steps: 13 + - uses: actions/checkout@v3 14 + 15 + - uses: pnpm/action-setup@v2 16 + with: 17 + version: 9 18 + run_install: false 19 + - uses: actions/setup-node@v3 20 + with: 21 + node-version: 18 22 + cache: pnpm 23 + 24 + - name: Install dependencies 25 + run: pnpm install --frozen-lockfile 26 + - name: Build moonlight 27 + env: 28 + NODE_ENV: production 29 + run: pnpm run build 30 + 31 + - name: Build MV3 32 + run: pnpm run browser 33 + - name: Build MV2 34 + run: pnpm run browser-mv2 35 + 36 + - name: Upload MV3 37 + uses: actions/upload-artifact@v4 38 + with: 39 + name: browser 40 + path: ./dist/browser 41 + - name: Upload MV2 42 + uses: actions/upload-artifact@v4 43 + with: 44 + name: browser-mv2 45 + path: ./dist/browser-mv2
+2
.github/workflows/nightly.yml
··· 31 31 - name: Build moonlight 32 32 env: 33 33 NODE_ENV: production 34 + MOONLIGHT_BRANCH: nightly 35 + MOONLIGHT_VERSION: ${{ github.sha }} 34 36 run: pnpm run build 35 37 36 38 - name: Write ref/commit to file
+2
.github/workflows/release.yml
··· 29 29 - name: Build moonlight 30 30 env: 31 31 NODE_ENV: production 32 + MOONLIGHT_BRANCH: stable 33 + MOONLIGHT_VERSION: ${{ github.ref_name }} 32 34 run: pnpm run build 33 35 - name: Create archive 34 36 run: |
+6 -2
README.md
··· 1 1 <h3 align="center"> 2 - <img src="./img/wordmark.png" alt="moonlight" /> 2 + <picture> 3 + <source media="(prefers-color-scheme: dark)" srcset="./img/wordmark-light.png"> 4 + <source media="(prefers-color-scheme: light)" srcset="./img/wordmark.png"> 5 + <img src="./img/wordmark.png" alt="moonlight" /> 6 + </picture> 3 7 4 8 <a href="https://discord.gg/FdZBTFCP6F">Discord server</a> 5 9 \- <a href="https://github.com/moonlight-mod/moonlight">GitHub</a> ··· 12 16 13 17 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 18 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. 19 + **_This is an experimental passion project._** Anything and everything is subject to change, but it is stable enough for developers to experiment with. 16 20 17 21 moonlight is licensed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.html) (`LGPL-3.0-or-later`). See [the documentation](https://moonlight-mod.github.io/) for more information.
+122 -27
build.mjs
··· 13 13 14 14 const prod = process.env.NODE_ENV === "production"; 15 15 const watch = process.argv.includes("--watch"); 16 + const browser = process.argv.includes("--browser"); 17 + const mv2 = process.argv.includes("--mv2"); 18 + 19 + const buildBranch = process.env.MOONLIGHT_BRANCH ?? "dev"; 20 + const buildVersion = process.env.MOONLIGHT_VERSION ?? "dev"; 16 21 17 22 const external = [ 18 23 "electron", 19 24 "fs", 20 25 "path", 21 26 "module", 22 - "events", 23 - "original-fs", // wtf asar? 24 27 "discord", // mappings 25 28 26 29 // Silence an esbuild warning ··· 74 77 }); 75 78 76 79 async function build(name, entry) { 77 - const outfile = path.join("./dist", name + ".js"); 80 + let outfile = path.join("./dist", name + ".js"); 81 + const browserDir = mv2 ? "browser-mv2" : "browser"; 82 + if (name === "browser") outfile = path.join("./dist", browserDir, "index.js"); 78 83 79 84 const dropLabels = []; 80 - if (name !== "injector") dropLabels.push("injector"); 81 - if (name !== "node-preload") dropLabels.push("nodePreload"); 82 - if (name !== "web-preload") dropLabels.push("webPreload"); 85 + const labels = { 86 + injector: ["injector"], 87 + nodePreload: ["node-preload"], 88 + webPreload: ["web-preload"], 89 + browser: ["browser"], 90 + 91 + webTarget: ["web-preload", "browser"], 92 + nodeTarget: ["node-preload", "injector"] 93 + }; 94 + for (const [label, targets] of Object.entries(labels)) { 95 + if (!targets.includes(name)) { 96 + dropLabels.push(label); 97 + } 98 + } 83 99 84 100 const define = { 85 101 MOONLIGHT_ENV: `"${name}"`, 86 - MOONLIGHT_PROD: prod.toString() 102 + MOONLIGHT_PROD: prod.toString(), 103 + MOONLIGHT_BRANCH: `"${buildBranch}"`, 104 + MOONLIGHT_VERSION: `"${buildVersion}"` 87 105 }; 88 106 89 - for (const iterName of Object.keys(config)) { 107 + for (const iterName of [ 108 + "injector", 109 + "node-preload", 110 + "web-preload", 111 + "browser" 112 + ]) { 90 113 const snake = iterName.replace(/-/g, "_").toUpperCase(); 91 114 define[`MOONLIGHT_${snake}`] = (name === iterName).toString(); 92 115 } 93 116 94 117 const nodeDependencies = ["glob"]; 95 118 const ignoredExternal = name === "web-preload" ? nodeDependencies : []; 119 + 120 + const plugins = [deduplicatedLogging, taggedBuildLog(name)]; 121 + if (name === "browser") { 122 + plugins.push( 123 + copyStaticFiles({ 124 + src: mv2 125 + ? "./packages/browser/manifestv2.json" 126 + : "./packages/browser/manifest.json", 127 + dest: `./dist/${browserDir}/manifest.json` 128 + }) 129 + ); 130 + 131 + if (!mv2) { 132 + plugins.push( 133 + copyStaticFiles({ 134 + src: "./packages/browser/modifyResponseHeaders.json", 135 + dest: `./dist/${browserDir}/modifyResponseHeaders.json` 136 + }) 137 + ); 138 + plugins.push( 139 + copyStaticFiles({ 140 + src: "./packages/browser/blockLoading.json", 141 + dest: `./dist/${browserDir}/blockLoading.json` 142 + }) 143 + ); 144 + } 145 + 146 + plugins.push( 147 + copyStaticFiles({ 148 + src: mv2 149 + ? "./packages/browser/src/background-mv2.js" 150 + : "./packages/browser/src/background.js", 151 + dest: `./dist/${browserDir}/background.js` 152 + }) 153 + ); 154 + } 96 155 97 156 /** @type {import("esbuild").BuildOptions} */ 98 157 const esbuildConfig = { 99 158 entryPoints: [entry], 100 159 outfile, 101 160 102 - format: "cjs", 103 - platform: name === "web-preload" ? "browser" : "node", 161 + format: "iife", 162 + globalName: "module.exports", 163 + 164 + platform: ["web-preload", "browser"].includes(name) ? "browser" : "node", 104 165 105 166 treeShaking: true, 106 167 bundle: true, ··· 113 174 dropLabels, 114 175 115 176 logLevel: "silent", 116 - plugins: [deduplicatedLogging, taggedBuildLog(name)] 177 + plugins 117 178 }; 179 + 180 + if (name === "browser") { 181 + const coreExtensionsJson = {}; 182 + 183 + // eslint-disable-next-line no-inner-declarations 184 + function readDir(dir) { 185 + const files = fs.readdirSync(dir); 186 + for (const file of files) { 187 + const filePath = dir + "/" + file; 188 + const normalizedPath = filePath.replace("./dist/core-extensions/", ""); 189 + if (fs.statSync(filePath).isDirectory()) { 190 + readDir(filePath); 191 + } else { 192 + coreExtensionsJson[normalizedPath] = fs.readFileSync( 193 + filePath, 194 + "utf8" 195 + ); 196 + } 197 + } 198 + } 199 + 200 + readDir("./dist/core-extensions"); 201 + 202 + esbuildConfig.banner = { 203 + js: `window._moonlight_coreExtensionsStr = ${JSON.stringify( 204 + JSON.stringify(coreExtensionsJson) 205 + )};` 206 + }; 207 + } 118 208 119 209 if (watch) { 120 210 const ctx = await esbuild.context(esbuildConfig); ··· 173 263 entryPoints, 174 264 outdir, 175 265 176 - format: "cjs", 266 + format: "iife", 267 + globalName: "module.exports", 177 268 platform: "node", 178 269 179 270 treeShaking: true, ··· 211 302 212 303 const promises = []; 213 304 214 - for (const [name, entry] of Object.entries(config)) { 215 - promises.push(build(name, entry)); 216 - } 305 + if (browser) { 306 + build("browser", "packages/browser/src/index.ts"); 307 + } else { 308 + for (const [name, entry] of Object.entries(config)) { 309 + promises.push(build(name, entry)); 310 + } 217 311 218 - const coreExtensions = fs.readdirSync("./packages/core-extensions/src"); 219 - for (const ext of coreExtensions) { 220 - let copiedManifest = false; 312 + const coreExtensions = fs.readdirSync("./packages/core-extensions/src"); 313 + for (const ext of coreExtensions) { 314 + let copiedManifest = false; 221 315 222 - for (const fileExt of ["ts", "tsx"]) { 223 - for (const type of ["index", "node", "host"]) { 224 - if ( 225 - fs.existsSync( 226 - `./packages/core-extensions/src/${ext}/${type}.${fileExt}` 227 - ) 228 - ) { 229 - promises.push(buildExt(ext, type, !copiedManifest, fileExt)); 230 - copiedManifest = true; 316 + for (const fileExt of ["ts", "tsx"]) { 317 + for (const type of ["index", "node", "host"]) { 318 + if ( 319 + fs.existsSync( 320 + `./packages/core-extensions/src/${ext}/${type}.${fileExt}` 321 + ) 322 + ) { 323 + promises.push(buildExt(ext, type, !copiedManifest, fileExt)); 324 + copiedManifest = true; 325 + } 231 326 } 232 327 } 233 328 }
+12 -12
flake.lock
··· 38 38 }, 39 39 "nixpkgs": { 40 40 "locked": { 41 - "lastModified": 1704295289, 42 - "narHash": "sha256-9WZDRfpMqCYL6g/HNWVvXF0hxdaAgwgIGeLYiOhmes8=", 41 + "lastModified": 1728067476, 42 + "narHash": "sha256-/uJcVXuBt+VFCPQIX+4YnYrHaubJSx4HoNsJVNRgANM=", 43 43 "owner": "NixOS", 44 44 "repo": "nixpkgs", 45 - "rev": "b0b2c5445c64191fd8d0b31f2b1a34e45a64547d", 45 + "rev": "6e6b3dd395c3b1eb9be9f2d096383a8d05add030", 46 46 "type": "github" 47 47 }, 48 48 "original": { 49 49 "owner": "NixOS", 50 - "ref": "nixos-23.11", 50 + "ref": "nixos-24.05", 51 51 "repo": "nixpkgs", 52 52 "type": "github" 53 53 } 54 54 }, 55 55 "nixpkgs_2": { 56 56 "locked": { 57 - "lastModified": 1702151865, 58 - "narHash": "sha256-9VAt19t6yQa7pHZLDbil/QctAgVsA66DLnzdRGqDisg=", 57 + "lastModified": 1727802920, 58 + "narHash": "sha256-HP89HZOT0ReIbI7IJZJQoJgxvB2Tn28V6XS3MNKnfLs=", 59 59 "owner": "nixos", 60 60 "repo": "nixpkgs", 61 - "rev": "666fc80e7b2afb570462423cb0e1cf1a3a34fedd", 61 + "rev": "27e30d177e57d912d614c88c622dcfdb2e6e6515", 62 62 "type": "github" 63 63 }, 64 64 "original": { ··· 74 74 "nixpkgs": "nixpkgs_2" 75 75 }, 76 76 "locked": { 77 - "lastModified": 1709572248, 78 - "narHash": "sha256-WhaKD4cIvZLbwI2vZTkpH/oEeqGiyMvdW3bLi24P0eU=", 79 - "owner": "mojotech", 77 + "lastModified": 1728137762, 78 + "narHash": "sha256-iEFvPR3BopGyI5KjQ1DK+gEZ1dKDugq838tKdet2moQ=", 79 + "owner": "NotNite", 80 80 "repo": "pnpm2nix-nzbr", 81 - "rev": "c3cfff81ea297cfb9dc18928652f375314dc287d", 81 + "rev": "b7a60d3c7d106b601665e3f05dba6cdc6f59f959", 82 82 "type": "github" 83 83 }, 84 84 "original": { 85 - "owner": "mojotech", 85 + "owner": "NotNite", 86 86 "repo": "pnpm2nix-nzbr", 87 87 "type": "github" 88 88 }
+5 -88
flake.nix
··· 2 2 description = "Yet another Discord mod"; 3 3 4 4 inputs = { 5 - nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; 5 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; 6 6 flake-utils.url = "github:numtide/flake-utils"; 7 - pnpm2nix.url = "github:mojotech/pnpm2nix-nzbr"; 7 + pnpm2nix.url = "github:NotNite/pnpm2nix-nzbr"; 8 8 }; 9 9 10 10 outputs = { self, nixpkgs, flake-utils, pnpm2nix }: 11 - let 12 - mkMoonlight = { pkgs, mkPnpmPackage }: 13 - mkPnpmPackage rec { 14 - workspace = ./.; 15 - src = ./.; 16 - components = [ 17 - "packages/core" 18 - "packages/core-extensions" 19 - "packages/injector" 20 - "packages/node-preload" 21 - "packages/types" 22 - "packages/web-preload" 23 - ]; 24 - distDirs = [ "dist" ]; 25 - 26 - copyNodeModules = true; 27 - buildPhase = "pnpm run build"; 28 - installPhase = "cp -r dist $out"; 29 - 30 - meta = with pkgs.lib; { 31 - description = "Yet another Discord mod"; 32 - homepage = "https://moonlight-mod.github.io/"; 33 - license = licenses.lgpl3; 34 - maintainers = with maintainers; [ notnite ]; 35 - }; 36 - }; 37 - 38 - nameTable = { 39 - discord = "Discord"; 40 - discord-ptb = "DiscordPTB"; 41 - discord-canary = "DiscordCanary"; 42 - discord-development = "DiscordDevelopment"; 43 - }; 44 - 45 - darwinNameTable = { 46 - discord = "Discord"; 47 - discord-ptb = "Discord PTB"; 48 - discord-canary = "Discord Canary"; 49 - discord-development = "Discord Development"; 50 - }; 51 - 52 - mkOverride = prev: moonlight: name: 53 - let discord = prev.${name}; 54 - in discord.overrideAttrs (old: { 55 - installPhase = let 56 - folderName = nameTable.${name}; 57 - darwinFolderName = darwinNameTable.${name}; 58 - 59 - injected = '' 60 - require("${moonlight}/injector").inject( 61 - require("path").join(__dirname, "../_app.asar") 62 - ); 63 - ''; 64 - 65 - packageJson = '' 66 - {"name":"discord","main":"./injector.js","private":true} 67 - ''; 68 - 69 - in old.installPhase + "\n" + '' 70 - resources="$out/opt/${folderName}/resources" 71 - if [ ! -d "$resources" ]; then 72 - resources="$out/Applications/${darwinFolderName}.app/Contents/Resources" 73 - fi 74 - 75 - mv "$resources/app.asar" "$resources/_app.asar" 76 - mkdir -p "$resources/app" 77 - 78 - cat > "$resources/app/injector.js" <<EOF 79 - ${injected} 80 - EOF 81 - 82 - echo '${packageJson}' > "$resources/app/package.json" 83 - ''; 84 - }); 85 - 86 - overlay = final: prev: rec { 87 - moonlight-mod = mkMoonlight { 88 - pkgs = final; 89 - mkPnpmPackage = pnpm2nix.packages.${final.system}.mkPnpmPackage; 90 - }; 91 - discord = mkOverride prev moonlight-mod "discord"; 92 - discord-ptb = mkOverride prev moonlight-mod "discord-ptb"; 93 - discord-canary = mkOverride prev moonlight-mod "discord-canary"; 94 - discord-development = 95 - mkOverride prev moonlight-mod "discord-development"; 96 - }; 11 + let overlay = import ./nix/overlay.nix { inherit pnpm2nix; }; 97 12 in flake-utils.lib.eachDefaultSystem (system: 98 13 let 99 14 pkgs = import nixpkgs { ··· 102 17 overlays = [ overlay ]; 103 18 }; 104 19 in { 20 + # Don't use these unless you're testing things 105 21 packages.default = pkgs.moonlight-mod; 106 22 packages.moonlight-mod = pkgs.moonlight-mod; 107 23 ··· 111 27 packages.discord-development = pkgs.discord-development; 112 28 }) // { 113 29 overlays.default = overlay; 30 + homeModules.default = ./nix/home-manager.nix; 114 31 }; 115 32 }
img/wordmark-light.png

This is a binary file and will not be displayed.

+28
nix/default.nix
··· 1 + { pkgs, mkPnpmPackage }: 2 + 3 + mkPnpmPackage rec { 4 + workspace = ./..; 5 + src = ./..; 6 + 7 + # Work around a bug with how it expects dist 8 + components = [ 9 + "packages/core" 10 + "packages/core-extensions" 11 + "packages/injector" 12 + "packages/node-preload" 13 + "packages/types" 14 + "packages/web-preload" 15 + ]; 16 + distDirs = [ "dist" ]; 17 + 18 + copyNodeModules = true; 19 + buildPhase = "pnpm run build"; 20 + installPhase = "cp -r dist $out"; 21 + 22 + meta = with pkgs.lib; { 23 + description = "Yet another Discord mod"; 24 + homepage = "https://moonlight-mod.github.io/"; 25 + license = licenses.lgpl3; 26 + maintainers = with maintainers; [ notnite ]; 27 + }; 28 + }
+56
nix/home-manager.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + let cfg = config.programs.moonlight-mod; 4 + in { 5 + options.programs.moonlight-mod = { 6 + enable = lib.mkEnableOption "Yet another Discord mod"; 7 + 8 + configs = let 9 + # TODO: type this 10 + type = lib.types.nullOr (lib.types.attrs); 11 + default = null; 12 + in { 13 + stable = lib.mkOption { 14 + inherit type default; 15 + description = "Configuration for Discord Stable"; 16 + }; 17 + 18 + ptb = lib.mkOption { 19 + inherit type default; 20 + description = "Configuration for Discord PTB"; 21 + }; 22 + 23 + canary = lib.mkOption { 24 + inherit type default; 25 + description = "Configuration for Discord Canary"; 26 + }; 27 + 28 + development = lib.mkOption { 29 + inherit type default; 30 + description = "Configuration for Discord Development"; 31 + }; 32 + }; 33 + }; 34 + 35 + config = lib.mkIf cfg.enable { 36 + xdg.configFile."moonlight-mod/stable.json" = 37 + lib.mkIf (cfg.configs.stable != null) { 38 + text = builtins.toJSON cfg.configs.stable; 39 + }; 40 + 41 + xdg.configFile."moonlight-mod/ptb.json" = 42 + lib.mkIf (cfg.configs.ptb != null) { 43 + text = builtins.toJSON cfg.configs.ptb; 44 + }; 45 + 46 + xdg.configFile."moonlight-mod/canary.json" = 47 + lib.mkIf (cfg.configs.canary != null) { 48 + text = builtins.toJSON cfg.configs.canary; 49 + }; 50 + 51 + xdg.configFile."moonlight-mod/development.json" = 52 + lib.mkIf (cfg.configs.development != null) { 53 + text = builtins.toJSON cfg.configs.development; 54 + }; 55 + }; 56 + }
+60
nix/overlay.nix
··· 1 + { pnpm2nix }: 2 + 3 + let 4 + nameTable = { 5 + discord = "Discord"; 6 + discord-ptb = "DiscordPTB"; 7 + discord-canary = "DiscordCanary"; 8 + discord-development = "DiscordDevelopment"; 9 + }; 10 + 11 + darwinNameTable = { 12 + discord = "Discord"; 13 + discord-ptb = "Discord PTB"; 14 + discord-canary = "Discord Canary"; 15 + discord-development = "Discord Development"; 16 + }; 17 + 18 + mkOverride = prev: moonlight: name: 19 + let discord = prev.${name}; 20 + in discord.overrideAttrs (old: { 21 + installPhase = let 22 + folderName = nameTable.${name}; 23 + darwinFolderName = darwinNameTable.${name}; 24 + 25 + injected = '' 26 + require("${moonlight}/injector").inject( 27 + require("path").join(__dirname, "../_app.asar") 28 + ); 29 + ''; 30 + 31 + packageJson = '' 32 + {"name":"discord","main":"./injector.js","private":true} 33 + ''; 34 + 35 + in old.installPhase + "\n" + '' 36 + resources="$out/opt/${folderName}/resources" 37 + if [ ! -d "$resources" ]; then 38 + resources="$out/Applications/${darwinFolderName}.app/Contents/Resources" 39 + fi 40 + 41 + mv "$resources/app.asar" "$resources/_app.asar" 42 + mkdir -p "$resources/app" 43 + 44 + cat > "$resources/app/injector.js" <<EOF 45 + ${injected} 46 + EOF 47 + 48 + echo '${packageJson}' > "$resources/app/package.json" 49 + ''; 50 + }); 51 + in final: prev: rec { 52 + moonlight-mod = final.callPackage ./default.nix { 53 + pkgs = final; 54 + mkPnpmPackage = pnpm2nix.packages.${final.system}.mkPnpmPackage; 55 + }; 56 + discord = mkOverride prev moonlight-mod "discord"; 57 + discord-ptb = mkOverride prev moonlight-mod "discord-ptb"; 58 + discord-canary = mkOverride prev moonlight-mod "discord-canary"; 59 + discord-development = mkOverride prev moonlight-mod "discord-development"; 60 + }
+2
package.json
··· 14 14 "scripts": { 15 15 "build": "node build.mjs", 16 16 "dev": "node build.mjs --watch", 17 + "browser": "node build.mjs --browser", 18 + "browser-mv2": "node build.mjs --browser --mv2", 17 19 "lint": "eslint packages", 18 20 "lint:fix": "eslint packages", 19 21 "lint:report": "eslint --output-file eslint_report.json --format json packages",
+13
packages/browser/blockLoading.json
··· 1 + [ 2 + { 3 + "id": 2, 4 + "priority": 1, 5 + "action": { 6 + "type": "block" 7 + }, 8 + "condition": { 9 + "urlFilter": "*://discord.com/assets/*.js", 10 + "resourceTypes": ["script"] 11 + } 12 + } 13 + ]
+49
packages/browser/manifest.json
··· 1 + { 2 + "manifest_version": 3, 3 + "name": "moonlight", 4 + "description": "Yet another Discord mod", 5 + "version": "1.1.0", 6 + "permissions": [ 7 + "declarativeNetRequestWithHostAccess", 8 + "webRequest", 9 + "scripting", 10 + "webNavigation" 11 + ], 12 + "host_permissions": [ 13 + "https://moonlight-mod.github.io/*", 14 + "https://api.github.com/*", 15 + "https://*.discord.com/*" 16 + ], 17 + "content_scripts": [ 18 + { 19 + "js": ["index.js"], 20 + "matches": ["https://*.discord.com/*"], 21 + "run_at": "document_start", 22 + "world": "MAIN" 23 + } 24 + ], 25 + "declarative_net_request": { 26 + "rule_resources": [ 27 + { 28 + "id": "modifyResponseHeaders", 29 + "enabled": true, 30 + "path": "modifyResponseHeaders.json" 31 + }, 32 + { 33 + "id": "blockLoading", 34 + "enabled": true, 35 + "path": "blockLoading.json" 36 + } 37 + ] 38 + }, 39 + "background": { 40 + "service_worker": "background.js", 41 + "type": "module" 42 + }, 43 + "web_accessible_resources": [ 44 + { 45 + "resources": ["index.js"], 46 + "matches": ["https://*.discord.com/*"] 47 + } 48 + ] 49 + }
+27
packages/browser/manifestv2.json
··· 1 + { 2 + "manifest_version": 2, 3 + "name": "moonlight", 4 + "description": "Yet another Discord mod", 5 + "version": "1.1.0", 6 + "permissions": [ 7 + "webRequest", 8 + "webRequestBlocking", 9 + "scripting", 10 + "webNavigation", 11 + "https://*.discord.com/assets/*.js", 12 + "https://moonlight-mod.github.io/*", 13 + "https://api.github.com/*", 14 + "https://*.discord.com/*" 15 + ], 16 + "background": { 17 + "scripts": ["background.js"] 18 + }, 19 + "content_scripts": [ 20 + { 21 + "js": ["index.js"], 22 + "matches": ["https://*.discord.com/*"], 23 + "run_at": "document_start", 24 + "world": "MAIN" 25 + } 26 + ] 27 + }
+19
packages/browser/modifyResponseHeaders.json
··· 1 + [ 2 + { 3 + "id": 1, 4 + "priority": 2, 5 + "action": { 6 + "type": "modifyHeaders", 7 + "responseHeaders": [ 8 + { 9 + "header": "Content-Security-Policy", 10 + "operation": "remove" 11 + } 12 + ] 13 + }, 14 + "condition": { 15 + "resourceTypes": ["main_frame"], 16 + "initiatorDomains": ["discord.com"] 17 + } 18 + } 19 + ]
+11
packages/browser/package.json
··· 1 + { 2 + "name": "@moonlight-mod/browser", 3 + "private": true, 4 + "dependencies": { 5 + "@moonlight-mod/core": "workspace:*", 6 + "@moonlight-mod/types": "workspace:*", 7 + "@moonlight-mod/web-preload": "workspace:*", 8 + "@zenfs/core": "^1.0.2", 9 + "@zenfs/dom": "^0.2.16" 10 + } 11 + }
+99
packages/browser/src/background-mv2.js
··· 1 + /* eslint-disable no-console */ 2 + /* eslint-disable no-undef */ 3 + 4 + const starterUrls = ["web.", "sentry."]; 5 + let blockLoading = true; 6 + let doing = false; 7 + let collectedUrls = new Set(); 8 + 9 + chrome.webNavigation.onBeforeNavigate.addListener(async (details) => { 10 + const url = new URL(details.url); 11 + if (!blockLoading && url.hostname.endsWith("discord.com")) { 12 + console.log("Blocking", details.url); 13 + blockLoading = true; 14 + collectedUrls.clear(); 15 + } 16 + }); 17 + 18 + async function doTheThing(urls, tabId) { 19 + console.log("Doing", urls, tabId); 20 + 21 + blockLoading = false; 22 + 23 + try { 24 + await chrome.scripting.executeScript({ 25 + target: { tabId }, 26 + world: "MAIN", 27 + args: [urls], 28 + func: async (urls) => { 29 + try { 30 + await window._moonlightBrowserInit(); 31 + } catch (e) { 32 + console.log(e); 33 + } 34 + 35 + const scripts = [...document.querySelectorAll("script")].filter( 36 + (script) => script.src && urls.some((url) => url.includes(script.src)) 37 + ); 38 + 39 + // backwards 40 + urls.reverse(); 41 + for (const url of urls) { 42 + const script = scripts.find((script) => url.includes(script.src)); 43 + console.log("adding new script", script); 44 + 45 + const newScript = document.createElement("script"); 46 + for (const { name, value } of script.attributes) { 47 + newScript.setAttribute(name, value); 48 + } 49 + 50 + script.remove(); 51 + document.documentElement.appendChild(newScript); 52 + } 53 + } 54 + }); 55 + } catch (e) { 56 + console.log(e); 57 + } 58 + 59 + doing = false; 60 + collectedUrls.clear(); 61 + } 62 + 63 + chrome.webRequest.onBeforeRequest.addListener( 64 + async (details) => { 65 + if (starterUrls.some((url) => details.url.includes(url))) { 66 + console.log("Adding", details.url); 67 + collectedUrls.add(details.url); 68 + } 69 + 70 + if (collectedUrls.size === starterUrls.length) { 71 + if (doing) return; 72 + if (!blockLoading) return; 73 + doing = true; 74 + const urls = [...collectedUrls]; 75 + const tabId = details.tabId; 76 + 77 + // yes this is a load-bearing sleep 78 + setTimeout(() => doTheThing(urls, tabId), 0); 79 + } 80 + 81 + if (blockLoading) return { cancel: true }; 82 + }, 83 + { 84 + urls: ["https://*.discord.com/assets/*.js"] 85 + }, 86 + ["blocking"] 87 + ); 88 + 89 + chrome.webRequest.onHeadersReceived.addListener( 90 + (details) => { 91 + return { 92 + responseHeaders: details.responseHeaders.filter( 93 + (header) => header.name.toLowerCase() !== "content-security-policy" 94 + ) 95 + }; 96 + }, 97 + { urls: ["https://*.discord.com/*"] }, 98 + ["blocking", "responseHeaders"] 99 + );
+114
packages/browser/src/background.js
··· 1 + /* eslint-disable no-console */ 2 + /* eslint-disable no-undef */ 3 + 4 + const starterUrls = ["web.", "sentry."]; 5 + let blockLoading = true; 6 + let doing = false; 7 + let collectedUrls = new Set(); 8 + 9 + chrome.webNavigation.onBeforeNavigate.addListener(async (details) => { 10 + const url = new URL(details.url); 11 + if (!blockLoading && url.hostname.endsWith("discord.com")) { 12 + await chrome.declarativeNetRequest.updateEnabledRulesets({ 13 + enableRulesetIds: ["modifyResponseHeaders", "blockLoading"] 14 + }); 15 + blockLoading = true; 16 + collectedUrls.clear(); 17 + } 18 + }); 19 + 20 + chrome.webRequest.onBeforeRequest.addListener( 21 + async (details) => { 22 + if (details.tabId === -1) return; 23 + if (starterUrls.some((url) => details.url.includes(url))) { 24 + console.log("Adding", details.url); 25 + collectedUrls.add(details.url); 26 + } 27 + 28 + if (collectedUrls.size === starterUrls.length) { 29 + if (doing) return; 30 + if (!blockLoading) return; 31 + doing = true; 32 + const urls = [...collectedUrls]; 33 + console.log("Doing", urls); 34 + 35 + console.log("Running moonlight script"); 36 + try { 37 + await chrome.scripting.executeScript({ 38 + target: { tabId: details.tabId }, 39 + world: "MAIN", 40 + files: ["index.js"] 41 + }); 42 + } catch (e) { 43 + console.log(e); 44 + } 45 + 46 + console.log("Initializing moonlight"); 47 + try { 48 + await chrome.scripting.executeScript({ 49 + target: { tabId: details.tabId }, 50 + world: "MAIN", 51 + func: async () => { 52 + try { 53 + await window._moonlightBrowserInit(); 54 + } catch (e) { 55 + console.log(e); 56 + } 57 + } 58 + }); 59 + } catch (e) { 60 + console.log(e); 61 + } 62 + 63 + console.log("Updating rulesets"); 64 + try { 65 + blockLoading = false; 66 + await chrome.declarativeNetRequest.updateEnabledRulesets({ 67 + disableRulesetIds: ["blockLoading"], 68 + enableRulesetIds: ["modifyResponseHeaders"] 69 + }); 70 + } catch (e) { 71 + console.log(e); 72 + } 73 + 74 + console.log("Readding scripts"); 75 + try { 76 + await chrome.scripting.executeScript({ 77 + target: { tabId: details.tabId }, 78 + world: "MAIN", 79 + args: [urls], 80 + func: async (urls) => { 81 + const scripts = [...document.querySelectorAll("script")].filter( 82 + (script) => 83 + script.src && urls.some((url) => url.includes(script.src)) 84 + ); 85 + 86 + // backwards 87 + urls.reverse(); 88 + for (const url of urls) { 89 + const script = scripts.find((script) => url.includes(script.src)); 90 + console.log("adding new script", script); 91 + 92 + const newScript = document.createElement("script"); 93 + for (const { name, value } of script.attributes) { 94 + newScript.setAttribute(name, value); 95 + } 96 + 97 + script.remove(); 98 + document.documentElement.appendChild(newScript); 99 + } 100 + } 101 + }); 102 + } catch (e) { 103 + console.log(e); 104 + } 105 + 106 + console.log("Done"); 107 + doing = false; 108 + collectedUrls.clear(); 109 + } 110 + }, 111 + { 112 + urls: ["*://*.discord.com/assets/*.js"] 113 + } 114 + );
+145
packages/browser/src/index.ts
··· 1 + import "@moonlight-mod/web-preload"; 2 + import { readConfig, writeConfig } from "@moonlight-mod/core/config"; 3 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 4 + import { getExtensions } from "@moonlight-mod/core/extension"; 5 + import { loadExtensions } from "@moonlight-mod/core/extension/loader"; 6 + import { MoonlightBranch, MoonlightNode } from "@moonlight-mod/types"; 7 + import { IndexedDB } from "@zenfs/dom"; 8 + import { configure } from "@zenfs/core"; 9 + import * as fs from "@zenfs/core/promises"; 10 + 11 + function getParts(path: string) { 12 + if (path.startsWith("/")) path = path.substring(1); 13 + return path.split("/"); 14 + } 15 + 16 + window._moonlightBrowserInit = async () => { 17 + // Set up a virtual filesystem with IndexedDB 18 + await configure({ 19 + mounts: { 20 + "/": { 21 + backend: IndexedDB, 22 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment 23 + // @ts-ignore tsc tweaking 24 + storeName: "moonlight-fs" 25 + } 26 + } 27 + }); 28 + 29 + window.moonlightFS = { 30 + async readFile(path) { 31 + return new Uint8Array(await fs.readFile(path)); 32 + }, 33 + async readFileString(path) { 34 + const file = await this.readFile(path); 35 + return new TextDecoder().decode(file); 36 + }, 37 + async writeFile(path, data) { 38 + await fs.writeFile(path, data); 39 + }, 40 + async writeFileString(path, data) { 41 + const file = new TextEncoder().encode(data); 42 + await this.writeFile(path, file); 43 + }, 44 + async unlink(path) { 45 + await fs.unlink(path); 46 + }, 47 + 48 + async readdir(path) { 49 + return await fs.readdir(path); 50 + }, 51 + async mkdir(path) { 52 + const parts = getParts(path); 53 + for (let i = 0; i < parts.length; i++) { 54 + const path = this.join(...parts.slice(0, i + 1)); 55 + if (!(await this.exists(path))) await fs.mkdir(path); 56 + } 57 + }, 58 + 59 + async rmdir(path) { 60 + const entries = await this.readdir(path); 61 + 62 + for (const entry of entries) { 63 + const fullPath = this.join(path, entry); 64 + const isFile = await this.isFile(fullPath); 65 + if (isFile) { 66 + await this.unlink(fullPath); 67 + } else { 68 + await this.rmdir(fullPath); 69 + } 70 + } 71 + 72 + await fs.rmdir(path); 73 + }, 74 + 75 + async exists(path) { 76 + return await fs.exists(path); 77 + }, 78 + async isFile(path) { 79 + return (await fs.stat(path)).isFile(); 80 + }, 81 + 82 + join(...parts) { 83 + let str = parts.join("/"); 84 + if (!str.startsWith("/")) str = "/" + str; 85 + return str; 86 + }, 87 + dirname(path) { 88 + const parts = getParts(path); 89 + return "/" + parts.slice(0, parts.length - 1).join("/"); 90 + } 91 + }; 92 + 93 + // Actual loading begins here 94 + const config = await readConfig(); 95 + initLogger(config); 96 + 97 + const extensions = await getExtensions(); 98 + const processedExtensions = await loadExtensions(extensions); 99 + 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 + const moonlightNode: MoonlightNode = { 107 + config, 108 + extensions, 109 + processedExtensions, 110 + nativesCache: {}, 111 + isBrowser: true, 112 + 113 + version: MOONLIGHT_VERSION, 114 + branch: MOONLIGHT_BRANCH as MoonlightBranch, 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 + getNatives: () => {}, 125 + getLogger: (id: string) => { 126 + return new Logger(id); 127 + }, 128 + 129 + getMoonlightDir() { 130 + return "/"; 131 + }, 132 + getExtensionDir: (ext: string) => { 133 + return `/extensions/${ext}`; 134 + }, 135 + 136 + writeConfig 137 + }; 138 + 139 + Object.assign(window, { 140 + moonlightNode 141 + }); 142 + 143 + // This is set by web-preload for us 144 + await window._moonlightBrowserLoad(); 145 + };
+6
packages/browser/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "compilerOptions": { 4 + "module": "ES2022" 5 + } 6 + }
+3 -2
packages/core-extensions/package.json
··· 2 2 "name": "@moonlight-mod/core-extensions", 3 3 "private": true, 4 4 "dependencies": { 5 - "@electron/asar": "^3.2.5", 6 - "@moonlight-mod/types": "workspace:*" 5 + "@moonlight-mod/core": "workspace:*", 6 + "@moonlight-mod/types": "workspace:*", 7 + "nanotar": "^0.1.1" 7 8 } 8 9 }
+2 -1
packages/core-extensions/src/contextMenu/index.tsx
··· 5 5 find: "Menu API only allows Items and groups of Items as children.", 6 6 replace: [ 7 7 { 8 - match: /(?<=let{navId[^}]+?}=(.),(.)=.\(.\))/, 8 + match: 9 + /(?<=let{navId[^}]+?}=(.),(.)=function .\(.\){.+(?=,.=function))/, 9 10 replacement: (_, props, items) => 10 11 `,__contextMenu=!${props}.__contextMenu_evilMenu&&require("contextMenu_contextMenu")._patchMenu(${props}, ${items})` 11 12 }
+2 -8
packages/core-extensions/src/contextMenu/webpackModules/contextMenu.ts
··· 31 31 } 32 32 33 33 export const patches: Patch[] = []; 34 - function _patchMenu(props: MenuProps, items: InternalItem[]) { 34 + export function _patchMenu(props: MenuProps, items: InternalItem[]) { 35 35 const matches = patches.filter((p) => p.navId === props.navId); 36 36 if (!matches.length) return; 37 37 ··· 43 43 } 44 44 45 45 let menuProps: any; 46 - function _saveProps(self: any, el: any) { 46 + export function _saveProps(self: any, el: any) { 47 47 menuProps = el.props; 48 48 49 49 const original = self.props.closeContextMenu; ··· 54 54 55 55 return el; 56 56 } 57 - 58 - module.exports = { 59 - addItem, 60 - _patchMenu, 61 - _saveProps 62 - }; 63 57 64 58 // Unmangle Menu elements 65 59 const code =
+6 -5
packages/core-extensions/src/contextMenu/webpackModules/evilMenu.ts
··· 6 6 "Menu API only allows Items and groups of Items as children." 7 7 )[0].id 8 8 ].toString(); 9 - code = code.replace( 10 - /onSelect:(.)}=(.),.=(.\(.\)),/, 11 - `onSelect:$1}=$2;return $3;let ` 12 - ); 9 + code = code.replace(/,.=(?=function .\(.\){.+?,.=function)/, ";return "); 10 + code = code.replace(/,(?=__contextMenu)/, ";let "); 13 11 const mod = new Function( 14 12 "module", 15 13 "exports", ··· 18 16 ); 19 17 const exp: any = {}; 20 18 mod({}, exp, require); 21 - const Menu = spacepack.findFunctionByStrings(exp, "isUsingKeyboardNavigation")!; 19 + const Menu = spacepack.findFunctionByStrings( 20 + exp, 21 + "Menu API only allows Items and groups of Items as children." 22 + )!; 22 23 module.exports = (el: any) => { 23 24 return Menu({ 24 25 children: el,
-17
packages/core-extensions/src/disableSentry/host.ts
··· 1 1 import { join } from "node:path"; 2 2 import { Module } from "node:module"; 3 - import { BrowserWindow } from "electron"; 4 3 5 4 const logger = moonlightHost.getLogger("disableSentry"); 6 5 ··· 24 23 logger.error("Failed to stub Sentry host side:", err); 25 24 } 26 25 } 27 - 28 - moonlightHost.events.on("window-created", (window: BrowserWindow) => { 29 - window.webContents.session.webRequest.onBeforeRequest( 30 - { 31 - urls: [ 32 - "https://*.sentry.io/*", 33 - "https://*.discord.com/error-reporting-proxy/*", 34 - "https://discord.com/assets/sentry.*.js", 35 - "https://*.discord.com/assets/sentry.*.js" 36 - ] 37 - }, 38 - function (details, callback) { 39 - callback({ cancel: true }); 40 - } 41 - ); 42 - });
+3 -4
packages/core-extensions/src/disableSentry/index.ts
··· 11 11 } 12 12 }, 13 13 { 14 - find: "window.DiscordSentry.addBreadcrumb", 14 + find: "this._sentryUtils=", 15 15 replace: { 16 16 type: PatchReplaceType.Normal, 17 - match: /Z:function\(\){return .}/, 18 - replacement: 19 - 'default:function(){return (...args)=>{moonlight.getLogger("disableSentry").debug("Sentry calling addBreadcrumb passthrough:", ...args);}}' 17 + match: /(?<=this._sentryUtils=)./, 18 + replacement: "undefined" 20 19 } 21 20 }, 22 21 {
+7 -1
packages/core-extensions/src/disableSentry/manifest.json
··· 6 6 "tagline": "Turns off Discord's error reporting systems", 7 7 "authors": ["Cynosphere", "NotNite"], 8 8 "tags": ["privacy"] 9 - } 9 + }, 10 + "blocked": [ 11 + "https://*.sentry.io/*", 12 + "https://*.discord.com/error-reporting-proxy/*", 13 + "https://discord.com/assets/sentry.*.js", 14 + "https://*.discord.com/assets/sentry.*.js" 15 + ] 10 16 }
+2 -2
packages/core-extensions/src/markdown/index.ts
··· 10 10 `=require("markdown_markdown")._addRules({newline:${rules}}),${RULES}=(0,` 11 11 }, 12 12 { 13 - match: /(?<=var (.{1,2})={RULES:.+?})/, 13 + match: /(?<=;(.{1,2}\.Z)={RULES:.+?})/, 14 14 replacement: (_, rulesets) => 15 15 `;require("markdown_markdown")._applyRulesetBlacklist(${rulesets});` 16 16 } ··· 26 26 }, 27 27 { 28 28 match: 29 - /(originalMatch:.}=(.);)(.+?)case"emoticon":(return .+?;)(.+?)case"link":{(.+?)}default:/, 29 + /(originalMatch:.}=(.);)(.+?)case"emoticon":(return .+?;)(.+?)case"subtext":{(.+?)}default:/, 30 30 replacement: ( 31 31 _, 32 32 start,
+68 -5
packages/core-extensions/src/moonbase/index.tsx
··· 1 - import { ExtensionWebExports } from "@moonlight-mod/types"; 1 + import { ExtensionWebpackModule } from "@moonlight-mod/types"; 2 2 3 - export const webpackModules: ExtensionWebExports["webpackModules"] = { 3 + export const webpackModules: Record<string, ExtensionWebpackModule> = { 4 4 stores: { 5 5 dependencies: [ 6 6 { id: "discord/packages/flux" }, ··· 21 21 ] 22 22 }, 23 23 24 - moonbase: { 24 + settings: { 25 25 dependencies: [ 26 26 { ext: "spacepack", id: "spacepack" }, 27 27 { ext: "settings", id: "settings" }, ··· 29 29 { ext: "moonbase", id: "ui" } 30 30 ], 31 31 entrypoint: true 32 + }, 33 + 34 + updates: { 35 + dependencies: [ 36 + { id: "react" }, 37 + { ext: "moonbase", id: "stores" }, 38 + { ext: "notices", id: "notices" }, 39 + { 40 + ext: "spacepack", 41 + id: "spacepack" 42 + } 43 + ], 44 + entrypoint: true 45 + }, 46 + 47 + moonbase: { 48 + dependencies: [{ ext: "moonbase", id: "stores" }] 32 49 } 33 50 }; 34 51 52 + const bg = "#222034"; 53 + const fg = "#FFFBA6"; 54 + 35 55 export const styles = [ 36 - ".moonbase-settings > :first-child { margin-top: 0px; }", 37 - "textarea.moonbase-resizeable { resize: vertical }" 56 + ` 57 + .moonbase-settings > :first-child { 58 + margin-top: 0px; 59 + } 60 + 61 + textarea.moonbase-resizeable { 62 + resize: vertical 63 + } 64 + 65 + .moonbase-updates-notice { 66 + background-color: ${bg}; 67 + color: ${fg}; 68 + line-height: unset; 69 + height: 36px; 70 + } 71 + 72 + .moonbase-updates-notice button { 73 + color: ${fg}; 74 + border-color: ${fg}; 75 + } 76 + 77 + .moonbase-updates-notice_text-wrapper { 78 + display: inline-flex; 79 + align-items: center; 80 + line-height: 36px; 81 + gap: 2px; 82 + } 83 + 84 + .moonbase-update-section { 85 + background-color: ${bg}; 86 + --info-help-foreground: ${fg}; 87 + border: none !important; 88 + color: ${fg}; 89 + 90 + display: flex; 91 + flex-direction: row; 92 + justify-content: space-between; 93 + } 94 + 95 + .moonbase-update-section > button { 96 + color: ${fg}; 97 + background-color: transparent; 98 + border-color: ${fg}; 99 + } 100 + `.trim() 38 101 ];
+19 -3
packages/core-extensions/src/moonbase/manifest.json
··· 4 4 "meta": { 5 5 "name": "Moonbase", 6 6 "tagline": "The official settings UI for moonlight", 7 - "authors": ["Cynosphere", "NotNite"] 7 + "authors": ["Cynosphere", "NotNite", "redstonekasi"] 8 8 }, 9 - "dependencies": ["spacepack", "settings", "common"], 9 + "dependencies": ["spacepack", "settings", "common", "notices"], 10 10 "settings": { 11 11 "sections": { 12 12 "displayName": "Split into sections", ··· 17 17 "displayName": "Persist filter", 18 18 "description": "Save extension filter in config", 19 19 "type": "boolean" 20 + }, 21 + "updateChecking": { 22 + "displayName": "Automatic update checking", 23 + "description": "Checks for updates to moonlight", 24 + "type": "boolean", 25 + "default": "true" 26 + }, 27 + "updateBanner": { 28 + "displayName": "Show update banner", 29 + "description": "Shows a banner for moonlight and extension updates", 30 + "type": "boolean", 31 + "default": "true" 20 32 } 21 - } 33 + }, 34 + "cors": [ 35 + "https://github.com/moonlight-mod/moonlight/releases/download/", 36 + "https://objects.githubusercontent.com/github-production-release-asset-" 37 + ] 22 38 }
+203
packages/core-extensions/src/moonbase/native.ts
··· 1 + import { MoonlightBranch } from "@moonlight-mod/types"; 2 + import type { MoonbaseNatives, RepositoryManifest } from "./types"; 3 + import extractAsar from "@moonlight-mod/core/asar"; 4 + import { 5 + distDir, 6 + repoUrlFile, 7 + installedVersionFile 8 + } from "@moonlight-mod/types/constants"; 9 + import { parseTarGzip } from "nanotar"; 10 + 11 + const githubRepo = "moonlight-mod/moonlight"; 12 + const githubApiUrl = `https://api.github.com/repos/${githubRepo}/releases/latest`; 13 + const artifactName = "dist.tar.gz"; 14 + 15 + const nightlyRefUrl = "https://moonlight-mod.github.io/moonlight/ref"; 16 + const nightlyZipUrl = "https://moonlight-mod.github.io/moonlight/dist.tar.gz"; 17 + 18 + export const userAgent = `moonlight/${moonlightNode.version} (https://github.com/moonlight-mod/moonlight)`; 19 + 20 + async function getStableRelease(): Promise<{ 21 + name: string; 22 + assets: { 23 + name: string; 24 + browser_download_url: string; 25 + }[]; 26 + }> { 27 + const req = await fetch(githubApiUrl, { 28 + cache: "no-store", 29 + headers: { 30 + "User-Agent": userAgent 31 + } 32 + }); 33 + return await req.json(); 34 + } 35 + 36 + export default function getNatives(): MoonbaseNatives { 37 + const logger = moonlightNode.getLogger("moonbase/natives"); 38 + 39 + return { 40 + async checkForMoonlightUpdate() { 41 + try { 42 + if (moonlightNode.branch === MoonlightBranch.STABLE) { 43 + const json = await getStableRelease(); 44 + return json.name !== moonlightNode.version ? json.name : null; 45 + } else if (moonlightNode.branch === MoonlightBranch.NIGHTLY) { 46 + const req = await fetch(nightlyRefUrl, { 47 + cache: "no-store", 48 + headers: { 49 + "User-Agent": userAgent 50 + } 51 + }); 52 + const ref = (await req.text()).split("\n")[0]; 53 + return ref !== moonlightNode.version ? ref : null; 54 + } 55 + 56 + return null; 57 + } catch (e) { 58 + logger.error("Error checking for moonlight update", e); 59 + return null; 60 + } 61 + }, 62 + 63 + async updateMoonlight() { 64 + // Note: this won't do anything on browser, we should probably disable it 65 + // entirely when running in browser. 66 + async function downloadStable(): Promise<[ArrayBuffer, string]> { 67 + const json = await getStableRelease(); 68 + const asset = json.assets.find((a) => a.name === artifactName); 69 + if (!asset) throw new Error("Artifact not found"); 70 + 71 + logger.debug(`Downloading ${asset.browser_download_url}`); 72 + const req = await fetch(asset.browser_download_url, { 73 + cache: "no-store", 74 + headers: { 75 + "User-Agent": userAgent 76 + } 77 + }); 78 + 79 + return [await req.arrayBuffer(), json.name]; 80 + } 81 + 82 + async function downloadNightly(): Promise<[ArrayBuffer, string]> { 83 + logger.debug(`Downloading ${nightlyZipUrl}`); 84 + const zipReq = await fetch(nightlyZipUrl, { 85 + cache: "no-store", 86 + headers: { 87 + "User-Agent": userAgent 88 + } 89 + }); 90 + 91 + const refReq = await fetch(nightlyRefUrl, { 92 + cache: "no-store", 93 + headers: { 94 + "User-Agent": userAgent 95 + } 96 + }); 97 + const ref = (await refReq.text()).split("\n")[0]; 98 + 99 + return [await zipReq.arrayBuffer(), ref]; 100 + } 101 + 102 + const [tar, ref] = 103 + moonlightNode.branch === MoonlightBranch.STABLE 104 + ? await downloadStable() 105 + : moonlightNode.branch === MoonlightBranch.NIGHTLY 106 + ? await downloadNightly() 107 + : [null, null]; 108 + 109 + if (!tar || !ref) return; 110 + 111 + const dist = moonlightFS.join(moonlightNode.getMoonlightDir(), distDir); 112 + if (await moonlightFS.exists(dist)) await moonlightFS.rmdir(dist); 113 + await moonlightFS.mkdir(dist); 114 + 115 + logger.debug("Extracting update"); 116 + const files = await parseTarGzip(tar); 117 + for (const file of files) { 118 + if (!file.data) continue; 119 + // @ts-expect-error What do you mean their own types are wrong 120 + if (file.type !== "file") continue; 121 + 122 + const fullFile = moonlightFS.join(dist, file.name); 123 + const fullDir = moonlightFS.dirname(fullFile); 124 + if (!(await moonlightFS.exists(fullDir))) 125 + await moonlightFS.mkdir(fullDir); 126 + await moonlightFS.writeFile(fullFile, file.data); 127 + } 128 + 129 + logger.debug("Writing version file:", ref); 130 + const versionFile = moonlightFS.join( 131 + moonlightNode.getMoonlightDir(), 132 + installedVersionFile 133 + ); 134 + await moonlightFS.writeFileString(versionFile, ref.trim()); 135 + 136 + logger.debug("Update extracted"); 137 + }, 138 + 139 + async fetchRepositories(repos) { 140 + const ret: Record<string, RepositoryManifest[]> = {}; 141 + 142 + for (const repo of repos) { 143 + try { 144 + const req = await fetch(repo, { 145 + cache: "no-store", 146 + headers: { 147 + "User-Agent": userAgent 148 + } 149 + }); 150 + const json = await req.json(); 151 + ret[repo] = json; 152 + } catch (e) { 153 + logger.error(`Error fetching repository ${repo}`, e); 154 + } 155 + } 156 + 157 + return ret; 158 + }, 159 + 160 + async installExtension(manifest, url, repo) { 161 + const req = await fetch(url, { 162 + headers: { 163 + "User-Agent": userAgent 164 + } 165 + }); 166 + 167 + const dir = moonlightNode.getExtensionDir(manifest.id); 168 + // remake it in case of updates 169 + if (await moonlightFS.exists(dir)) await moonlightFS.rmdir(dir); 170 + await moonlightFS.mkdir(dir); 171 + 172 + const buffer = await req.arrayBuffer(); 173 + const files = extractAsar(buffer); 174 + for (const [file, buf] of Object.entries(files)) { 175 + const fullFile = moonlightFS.join(dir, file); 176 + const fullDir = moonlightFS.dirname(fullFile); 177 + 178 + if (!(await moonlightFS.exists(fullDir))) 179 + await moonlightFS.mkdir(fullDir); 180 + await moonlightFS.writeFile(moonlightFS.join(dir, file), buf); 181 + } 182 + 183 + await moonlightFS.writeFileString( 184 + moonlightFS.join(dir, repoUrlFile), 185 + repo 186 + ); 187 + }, 188 + 189 + async deleteExtension(id) { 190 + const dir = moonlightNode.getExtensionDir(id); 191 + await moonlightFS.rmdir(dir); 192 + }, 193 + 194 + getExtensionConfig(id, key) { 195 + const config = moonlightNode.config.extensions[id]; 196 + if (typeof config === "object") { 197 + return config.config?.[key]; 198 + } 199 + 200 + return undefined; 201 + } 202 + }; 203 + }
+2 -69
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 - const logger = moonlightNode.getLogger("moonbase"); 9 - 10 - async function fetchRepositories(repos: string[]) { 11 - const ret: Record<string, RepositoryManifest[]> = {}; 12 - 13 - for (const repo of repos) { 14 - try { 15 - const req = await fetch(repo); 16 - const json = await req.json(); 17 - ret[repo] = json; 18 - } catch (e) { 19 - logger.error(`Error fetching repository ${repo}`, e); 20 - } 21 - } 22 - 23 - return ret; 24 - } 25 - 26 - async function installExtension( 27 - manifest: RepositoryManifest, 28 - url: string, 29 - repo: string 30 - ) { 31 - const req = await fetch(url); 32 - 33 - const dir = moonlightNode.getExtensionDir(manifest.id); 34 - // remake it in case of updates 35 - if (fs.existsSync(dir)) fs.rmdirSync(dir, { recursive: true }); 36 - fs.mkdirSync(dir, { recursive: true }); 37 - 38 - // for some reason i just can't .writeFileSync() a file that ends in .asar??? 39 - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "moonlight-")); 40 - const tempFile = path.join(tempDir, "extension"); 41 - const buffer = await req.arrayBuffer(); 42 - fs.writeFileSync(tempFile, Buffer.from(buffer)); 43 - 44 - asar.extractAll(tempFile, dir); 45 - fs.writeFileSync(path.join(dir, repoUrlFile), repo); 46 - } 47 - 48 - async function deleteExtension(id: string) { 49 - const dir = moonlightNode.getExtensionDir(id); 50 - fs.rmdirSync(dir, { recursive: true }); 51 - } 52 - 53 - function getExtensionConfig(id: string, key: string): any { 54 - const config = moonlightNode.config.extensions[id]; 55 - if (typeof config === "object") { 56 - return config.config?.[key]; 57 - } 58 - 59 - return undefined; 60 - } 61 - 62 - const exports: MoonbaseNatives = { 63 - fetchRepositories, 64 - installExtension, 65 - deleteExtension, 66 - getExtensionConfig 67 - }; 68 - 69 - module.exports = exports; 1 + import getNatives from "./native"; 2 + module.exports = getNatives();
+7 -1
packages/core-extensions/src/moonbase/types.ts
··· 1 - import { DetectedExtension, ExtensionManifest } from "types/src"; 1 + import { ExtensionCompat } from "@moonlight-mod/core/extension/loader"; 2 + import { DetectedExtension, ExtensionManifest } from "@moonlight-mod/types"; 2 3 3 4 export type MoonbaseNatives = { 5 + checkForMoonlightUpdate(): Promise<string | null>; 6 + updateMoonlight(): Promise<void>; 7 + 4 8 fetchRepositories( 5 9 repos: string[] 6 10 ): Promise<Record<string, RepositoryManifest[]>>; ··· 29 33 manifest: ExtensionManifest | RepositoryManifest; 30 34 source: DetectedExtension["source"]; 31 35 state: ExtensionState; 36 + compat: ExtensionCompat; 37 + hasUpdate: boolean; 32 38 };
+10
packages/core-extensions/src/moonbase/webpackModules/moonbase.ts
··· 1 + import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 2 + import type { Moonbase } from "@moonlight-mod/types/coreExtensions/moonbase"; 3 + 4 + export const moonbase: Moonbase = { 5 + registerConfigComponent(ext, option, component) { 6 + MoonbaseSettingsStore.registerConfigComponent(ext, option, component); 7 + } 8 + }; 9 + 10 + export default moonbase;
-44
packages/core-extensions/src/moonbase/webpackModules/moonbase.tsx
··· 1 - import settings from "@moonlight-mod/wp/settings_settings"; 2 - import React from "@moonlight-mod/wp/react"; 3 - import spacepack from "@moonlight-mod/wp/spacepack_spacepack"; 4 - import { Moonbase, pages } from "@moonlight-mod/wp/moonbase_ui"; 5 - 6 - import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 7 - import { MenuItem } from "@moonlight-mod/wp/discord/components/common/index"; 8 - 9 - const { open } = spacepack.findByExports("setSection", "clearSubsection")[0] 10 - .exports.Z; 11 - 12 - settings.addSection("moonbase", "Moonbase", Moonbase, null, -2, { 13 - stores: [MoonbaseSettingsStore], 14 - element: () => { 15 - // Require it here because lazy loading SUX 16 - const SettingsNotice = spacepack.findByCode( 17 - "onSaveButtonColor", 18 - "FocusRingScope" 19 - )[0].exports.Z; 20 - return ( 21 - <SettingsNotice 22 - submitting={MoonbaseSettingsStore.submitting} 23 - onReset={() => { 24 - MoonbaseSettingsStore.reset(); 25 - }} 26 - onSave={() => { 27 - MoonbaseSettingsStore.writeConfig(); 28 - }} 29 - /> 30 - ); 31 - } 32 - }); 33 - 34 - settings.addSectionMenuItems( 35 - "moonbase", 36 - ...pages.map((page, i) => ( 37 - <MenuItem 38 - key={page.id} 39 - id={`moonbase-${page.id}`} 40 - label={page.name} 41 - action={() => open("moonbase", i)} 42 - /> 43 - )) 44 - );
+113
packages/core-extensions/src/moonbase/webpackModules/settings.tsx
··· 1 + import settings from "@moonlight-mod/wp/settings_settings"; 2 + import React from "@moonlight-mod/wp/react"; 3 + import spacepack from "@moonlight-mod/wp/spacepack_spacepack"; 4 + import { Moonbase, pages } from "@moonlight-mod/wp/moonbase_ui"; 5 + 6 + import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 7 + import * as Components from "@moonlight-mod/wp/discord/components/common/index"; 8 + 9 + import Update from "./ui/update"; 10 + 11 + const { MenuItem, Text, Breadcrumbs } = Components; 12 + 13 + const Margins = spacepack.require("discord/styles/shared/Margins.css"); 14 + 15 + const { open } = spacepack.findByExports("setSection", "clearSubsection")[0] 16 + .exports.Z; 17 + 18 + const notice = { 19 + stores: [MoonbaseSettingsStore], 20 + element: () => { 21 + // Require it here because lazy loading SUX 22 + const SettingsNotice = spacepack.findByCode( 23 + "onSaveButtonColor", 24 + "FocusRingScope" 25 + )[0].exports.Z; 26 + return ( 27 + <SettingsNotice 28 + submitting={MoonbaseSettingsStore.submitting} 29 + onReset={() => { 30 + MoonbaseSettingsStore.reset(); 31 + }} 32 + onSave={() => { 33 + MoonbaseSettingsStore.writeConfig(); 34 + }} 35 + /> 36 + ); 37 + } 38 + }; 39 + 40 + function addSection( 41 + id: string, 42 + name: string, 43 + element: React.FunctionComponent 44 + ) { 45 + settings.addSection(`moonbase-${id}`, name, element, null, -2, notice); 46 + } 47 + 48 + // FIXME: move to component types 49 + type Breadcrumb = { 50 + id: string; 51 + label: string; 52 + }; 53 + 54 + function renderBreadcrumb(crumb: Breadcrumb, last: boolean) { 55 + return ( 56 + <Text 57 + variant="heading-lg/semibold" 58 + tag="h2" 59 + color={last ? "header-primary" : "header-secondary"} 60 + > 61 + {crumb.label} 62 + </Text> 63 + ); 64 + } 65 + 66 + if ( 67 + MoonbaseSettingsStore.getExtensionConfigRaw<boolean>( 68 + "moonbase", 69 + "sections", 70 + false 71 + ) 72 + ) { 73 + settings.addHeader("Moonbase", -2); 74 + 75 + for (const page of pages) { 76 + addSection(page.id, page.name, () => { 77 + const breadcrumbs = [ 78 + { id: "moonbase", label: "Moonbase" }, 79 + { id: page.id, label: page.name } 80 + ]; 81 + return ( 82 + <> 83 + <Breadcrumbs 84 + className={Margins.marginBottom20} 85 + renderCustomBreadcrumb={renderBreadcrumb} 86 + breadcrumbs={breadcrumbs} 87 + activeId={page.id} 88 + > 89 + {page.name} 90 + </Breadcrumbs> 91 + 92 + <Update /> 93 + 94 + <page.element /> 95 + </> 96 + ); 97 + }); 98 + } 99 + } else { 100 + settings.addSection("moonbase", "Moonbase", Moonbase, null, -2, notice); 101 + 102 + settings.addSectionMenuItems( 103 + "moonbase", 104 + ...pages.map((page, i) => ( 105 + <MenuItem 106 + key={page.id} 107 + id={`moonbase-${page.id}`} 108 + label={page.name} 109 + action={() => open("moonbase", i)} 110 + /> 111 + )) 112 + ); 113 + }
+188 -47
packages/core-extensions/src/moonbase/webpackModules/stores.ts
··· 1 1 import { Config, ExtensionLoadSource } from "@moonlight-mod/types"; 2 - import { ExtensionState, MoonbaseExtension, MoonbaseNatives } from "../types"; 2 + import { 3 + ExtensionState, 4 + MoonbaseExtension, 5 + MoonbaseNatives, 6 + RepositoryManifest 7 + } from "../types"; 3 8 import { Store } from "@moonlight-mod/wp/discord/packages/flux"; 4 9 import Dispatcher from "@moonlight-mod/wp/discord/Dispatcher"; 10 + import getNatives from "../native"; 11 + import { mainRepo } from "@moonlight-mod/types/constants"; 12 + import { 13 + checkExtensionCompat, 14 + ExtensionCompat 15 + } from "@moonlight-mod/core/extension/loader"; 16 + import { CustomComponent } from "@moonlight-mod/types/coreExtensions/moonbase"; 5 17 6 - const natives: MoonbaseNatives = moonlight.getNatives("moonbase"); 7 18 const logger = moonlight.getLogger("moonbase"); 8 19 20 + let natives: MoonbaseNatives = moonlight.getNatives("moonbase"); 21 + if (moonlightNode.isBrowser) natives = getNatives(); 22 + 9 23 class MoonbaseSettingsStore extends Store<any> { 10 24 private origConfig: Config; 11 25 private config: Config; 12 26 private extensionIndex: number; 27 + private configComponents: Record<string, Record<string, CustomComponent>> = 28 + {}; 13 29 14 30 modified: boolean; 15 31 submitting: boolean; 16 32 installing: boolean; 33 + 34 + newVersion: string | null; 35 + shouldShowNotice: boolean; 17 36 18 37 extensions: { [id: number]: MoonbaseExtension }; 19 - updates: { [id: number]: { version: string; download: string } }; 38 + updates: { 39 + [id: number]: { 40 + version: string; 41 + download: string; 42 + updateManifest: RepositoryManifest; 43 + }; 44 + }; 20 45 21 46 constructor() { 22 47 super(Dispatcher); ··· 28 53 this.modified = false; 29 54 this.submitting = false; 30 55 this.installing = false; 56 + 57 + this.newVersion = null; 58 + this.shouldShowNotice = false; 31 59 32 60 this.extensions = {}; 33 61 this.updates = {}; ··· 38 66 uniqueId, 39 67 state: moonlight.enabledExtensions.has(ext.id) 40 68 ? ExtensionState.Enabled 41 - : ExtensionState.Disabled 69 + : ExtensionState.Disabled, 70 + compat: checkExtensionCompat(ext.manifest), 71 + hasUpdate: false 42 72 }; 43 73 } 44 74 45 - natives.fetchRepositories(this.config.repositories).then((ret) => { 46 - for (const [repo, exts] of Object.entries(ret)) { 47 - try { 48 - for (const ext of exts) { 49 - const level = ext.apiLevel ?? 1; 50 - if (level !== window.moonlight.apiLevel) continue; 75 + natives! 76 + .fetchRepositories(this.config.repositories) 77 + .then((ret) => { 78 + for (const [repo, exts] of Object.entries(ret)) { 79 + try { 80 + for (const ext of exts) { 81 + const uniqueId = this.extensionIndex++; 82 + const extensionData = { 83 + id: ext.id, 84 + uniqueId, 85 + manifest: ext, 86 + source: { type: ExtensionLoadSource.Normal, url: repo }, 87 + state: ExtensionState.NotDownloaded, 88 + compat: ExtensionCompat.Compatible, 89 + hasUpdate: false 90 + }; 51 91 52 - const uniqueId = this.extensionIndex++; 53 - const extensionData = { 54 - id: ext.id, 55 - uniqueId, 56 - manifest: ext, 57 - source: { type: ExtensionLoadSource.Normal, url: repo }, 58 - state: ExtensionState.NotDownloaded 59 - }; 92 + // Don't present incompatible updates 93 + if (checkExtensionCompat(ext) !== ExtensionCompat.Compatible) 94 + continue; 95 + 96 + const existing = this.getExisting(extensionData); 97 + if (existing != null) { 98 + // Make sure the download URL is properly updated 99 + for (const [id, e] of Object.entries(this.extensions)) { 100 + if (e.id === ext.id && e.source.url === repo) { 101 + this.extensions[parseInt(id)].manifest = { 102 + ...e.manifest, 103 + download: ext.download 104 + }; 105 + break; 106 + } 107 + } 108 + 109 + if (this.hasUpdate(extensionData)) { 110 + this.updates[existing.uniqueId] = { 111 + version: ext.version!, 112 + download: ext.download, 113 + updateManifest: ext 114 + }; 115 + existing.hasUpdate = true; 116 + } 60 117 61 - if (this.alreadyExists(extensionData)) { 62 - if (this.hasUpdate(extensionData)) { 63 - this.updates[uniqueId] = { 64 - version: ext.version!, 65 - download: ext.download 66 - }; 118 + continue; 67 119 } 68 120 69 - continue; 121 + this.extensions[uniqueId] = extensionData; 70 122 } 71 - 72 - this.extensions[uniqueId] = extensionData; 123 + } catch (e) { 124 + logger.error(`Error processing repository ${repo}`, e); 73 125 } 74 - } catch (e) { 75 - logger.error(`Error processing repository ${repo}`, e); 76 126 } 77 - } 78 127 79 - this.emitChange(); 80 - }); 128 + this.emitChange(); 129 + }) 130 + .then(() => 131 + this.getExtensionConfigRaw("moonbase", "updateChecking", true) 132 + ? natives!.checkForMoonlightUpdate() 133 + : new Promise<null>((resolve) => resolve(null)) 134 + ) 135 + .then((version) => { 136 + this.newVersion = version; 137 + this.emitChange(); 138 + }) 139 + .then(() => { 140 + this.shouldShowNotice = 141 + this.newVersion != null || Object.keys(this.updates).length > 0; 142 + this.emitChange(); 143 + }); 81 144 } 82 145 83 - private alreadyExists(ext: MoonbaseExtension) { 84 - return Object.values(this.extensions).some( 146 + private getExisting(ext: MoonbaseExtension) { 147 + return Object.values(this.extensions).find( 85 148 (e) => e.id === ext.id && e.source.url === ext.source.url 86 149 ); 87 150 } ··· 160 223 return cfg.config?.[key] ?? clonedDefaultValue; 161 224 } 162 225 226 + getExtensionConfigRaw<T>( 227 + id: string, 228 + key: string, 229 + defaultValue: T | undefined 230 + ): T | undefined { 231 + const cfg = this.config.extensions[id]; 232 + 233 + if (cfg == null || typeof cfg === "boolean") return defaultValue; 234 + return cfg.config?.[key] ?? defaultValue; 235 + } 236 + 163 237 getExtensionConfigName(uniqueId: number, key: string) { 164 238 const ext = this.getExtension(uniqueId); 165 239 return ext.manifest.settings?.[key]?.displayName ?? key; ··· 170 244 return ext.manifest.settings?.[key]?.description; 171 245 } 172 246 173 - setExtensionConfig(uniqueId: number, key: string, value: any) { 174 - const ext = this.getExtension(uniqueId); 175 - const oldConfig = this.config.extensions[ext.id]; 247 + setExtensionConfig(id: string, key: string, value: any) { 248 + const oldConfig = this.config.extensions[id]; 176 249 const newConfig = 177 250 typeof oldConfig === "boolean" 178 251 ? { ··· 184 257 config: { ...(oldConfig?.config ?? {}), [key]: value } 185 258 }; 186 259 187 - this.config.extensions[ext.id] = newConfig; 260 + this.config.extensions[id] = newConfig; 188 261 this.modified = this.isModified(); 189 262 this.emitChange(); 190 263 } ··· 219 292 220 293 this.installing = true; 221 294 try { 222 - const url = this.updates[uniqueId]?.download ?? ext.manifest.download; 223 - await natives.installExtension(ext.manifest, url, ext.source.url!); 295 + const update = this.updates[uniqueId]; 296 + const url = update?.download ?? ext.manifest.download; 297 + await natives!.installExtension(ext.manifest, url, ext.source.url!); 224 298 if (ext.state === ExtensionState.NotDownloaded) { 225 299 this.extensions[uniqueId].state = ExtensionState.Disabled; 226 300 } 227 301 302 + if (update != null) 303 + this.extensions[uniqueId].compat = checkExtensionCompat( 304 + update.updateManifest 305 + ); 306 + 228 307 delete this.updates[uniqueId]; 229 308 } catch (e) { 230 309 logger.error("Error installing extension:", e); ··· 234 313 this.emitChange(); 235 314 } 236 315 316 + private getRank(ext: MoonbaseExtension) { 317 + if (ext.source.type === ExtensionLoadSource.Developer) return 3; 318 + if (ext.source.type === ExtensionLoadSource.Core) return 2; 319 + if (ext.source.url === mainRepo) return 1; 320 + return 0; 321 + } 322 + 323 + async getDependencies(uniqueId: number) { 324 + const ext = this.getExtension(uniqueId); 325 + 326 + const missingDeps = []; 327 + for (const dep of ext.manifest.dependencies ?? []) { 328 + const anyInstalled = Object.values(this.extensions).some( 329 + (e) => e.id === dep && e.state !== ExtensionState.NotDownloaded 330 + ); 331 + if (!anyInstalled) missingDeps.push(dep); 332 + } 333 + 334 + if (missingDeps.length === 0) return null; 335 + 336 + const deps: Record<string, MoonbaseExtension[]> = {}; 337 + for (const dep of missingDeps) { 338 + const candidates = Object.values(this.extensions).filter( 339 + (e) => e.id === dep 340 + ); 341 + 342 + deps[dep] = candidates.sort((a, b) => { 343 + const aRank = this.getRank(a); 344 + const bRank = this.getRank(b); 345 + if (aRank === bRank) { 346 + const repoIndex = this.config.repositories.indexOf(a.source.url!); 347 + const otherRepoIndex = this.config.repositories.indexOf( 348 + b.source.url! 349 + ); 350 + return repoIndex - otherRepoIndex; 351 + } else { 352 + return bRank - aRank; 353 + } 354 + }); 355 + } 356 + 357 + return deps; 358 + } 359 + 237 360 async deleteExtension(uniqueId: number) { 238 361 const ext = this.getExtension(uniqueId); 239 362 if (ext == null) return; 240 363 241 364 this.installing = true; 242 365 try { 243 - await natives.deleteExtension(ext.id); 366 + await natives!.deleteExtension(ext.id); 244 367 this.extensions[uniqueId].state = ExtensionState.NotDownloaded; 245 368 } catch (e) { 246 369 logger.error("Error deleting extension:", e); ··· 250 373 this.emitChange(); 251 374 } 252 375 376 + async updateMoonlight() { 377 + await natives.updateMoonlight(); 378 + } 379 + 253 380 getConfigOption<K extends keyof Config>(key: K): Config[K] { 254 381 return this.config[key]; 255 382 } ··· 260 387 this.emitChange(); 261 388 } 262 389 390 + tryGetExtensionName(id: string) { 391 + const uniqueId = this.getExtensionUniqueId(id); 392 + return (uniqueId != null ? this.getExtensionName(uniqueId) : null) ?? id; 393 + } 394 + 395 + registerConfigComponent( 396 + ext: string, 397 + name: string, 398 + component: CustomComponent 399 + ) { 400 + if (!(ext in this.configComponents)) this.configComponents[ext] = {}; 401 + this.configComponents[ext][name] = component; 402 + } 403 + 404 + getExtensionConfigComponent(ext: string, name: string) { 405 + return this.configComponents[ext]?.[name]; 406 + } 407 + 263 408 writeConfig() { 264 409 this.submitting = true; 265 410 266 - try { 267 - moonlightNode.writeConfig(this.config); 268 - this.origConfig = this.clone(this.config); 269 - } catch (e) { 270 - logger.error("Error writing config", e); 271 - } 411 + moonlightNode.writeConfig(this.config); 412 + this.origConfig = this.clone(this.config); 272 413 273 414 this.submitting = false; 274 415 this.modified = false;
+18
packages/core-extensions/src/moonbase/webpackModules/ui/config/index.tsx
··· 121 121 export default function ConfigPage() { 122 122 return ( 123 123 <> 124 + <FormSwitch 125 + className={Margins.marginTop20} 126 + value={MoonbaseSettingsStore.getExtensionConfigRaw<boolean>( 127 + "moonbase", 128 + "updateChecking", 129 + true 130 + )} 131 + onChange={(value: boolean) => { 132 + MoonbaseSettingsStore.setExtensionConfig( 133 + "moonbase", 134 + "updateChecking", 135 + value 136 + ); 137 + }} 138 + note="Checks for updates to moonlight" 139 + > 140 + Automatic update checking 141 + </FormSwitch> 124 142 <FormItem title="Repositories"> 125 143 <FormText className={Margins.marginBottom4}> 126 144 A list of remote repositories to display extensions from
+94 -16
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/card.tsx
··· 1 1 import { ExtensionState } from "../../../types"; 2 2 import { ExtensionLoadSource } from "@moonlight-mod/types"; 3 + import { ExtensionCompat } from "@moonlight-mod/core/extension/loader"; 3 4 4 5 import spacepack from "@moonlight-mod/wp/spacepack_spacepack"; 5 6 import * as Components from "@moonlight-mod/wp/discord/components/common/index"; ··· 11 12 12 13 import ExtensionInfo from "./info"; 13 14 import Settings from "./settings"; 15 + import installWithDependencyPopup from "./popup"; 14 16 15 17 export enum ExtensionPage { 16 18 Info, ··· 20 22 21 23 import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 22 24 23 - const { DownloadIcon, TrashIcon, CircleWarningIcon } = Components; 25 + const { BeakerIcon, DownloadIcon, TrashIcon, CircleWarningIcon, Tooltip } = 26 + Components; 24 27 25 28 const PanelButton = spacepack.findByCode("Masks.PANEL_BUTTON")[0].exports.Z; 26 29 const TabBarClasses = spacepack.findByExports( ··· 28 31 "tabBarItem", 29 32 "headerContentWrapper" 30 33 )[0].exports; 34 + const MarkupClasses = spacepack.findByExports("markup", "inlineFormat")[0] 35 + .exports; 36 + 37 + const BuildOverrideClasses = spacepack.findByExports( 38 + "disabledButtonOverride" 39 + )[0].exports; 40 + 41 + const COMPAT_TEXT_MAP: Record<ExtensionCompat, string> = { 42 + [ExtensionCompat.Compatible]: "huh?", 43 + [ExtensionCompat.InvalidApiLevel]: "Incompatible API level", 44 + [ExtensionCompat.InvalidEnvironment]: "Incompatible platform" 45 + }; 31 46 32 47 export default function ExtensionCard({ uniqueId }: { uniqueId: number }) { 33 48 const [tab, setTab] = React.useState(ExtensionPage.Info); ··· 49 64 // Why it work like that :sob: 50 65 if (ext == null) return <></>; 51 66 52 - const { Card, Text, Switch, TabBar, Button } = Components; 67 + const { Card, Text, FormSwitch, TabBar, Button } = Components; 53 68 54 69 const tagline = ext.manifest?.meta?.tagline; 55 70 const settings = ext.manifest?.settings; 56 71 const description = ext.manifest?.meta?.description; 72 + const enabledDependants = useStateFromStores([MoonbaseSettingsStore], () => 73 + Object.keys(MoonbaseSettingsStore.extensions) 74 + .filter((uniqueId) => { 75 + const potentialDependant = MoonbaseSettingsStore.getExtension( 76 + parseInt(uniqueId) 77 + ); 78 + 79 + return ( 80 + potentialDependant.manifest.dependencies?.includes(ext.id) && 81 + MoonbaseSettingsStore.getExtensionEnabled(parseInt(uniqueId)) 82 + ); 83 + }) 84 + .map((a) => MoonbaseSettingsStore.getExtension(parseInt(a))) 85 + ); 86 + const implicitlyEnabled = enabledDependants.length > 0; 57 87 58 88 return ( 59 89 <Card editable={true} className={IntegrationCard.card}> 60 90 <div className={IntegrationCard.cardHeader}> 61 91 <Flex direction={Flex.Direction.VERTICAL}> 62 - <Flex direction={Flex.Direction.HORIZONTAL}> 92 + <Flex direction={Flex.Direction.HORIZONTAL} align={Flex.Align.CENTER}> 63 93 <Text variant="text-md/semibold"> 64 94 {ext.manifest?.meta?.name ?? ext.id} 65 95 </Text> 96 + {ext.source.type === ExtensionLoadSource.Developer && ( 97 + <Tooltip text="This is a local extension" position="top"> 98 + {(props: any) => ( 99 + <BeakerIcon 100 + {...props} 101 + class={BuildOverrideClasses.infoIcon} 102 + size="xs" 103 + /> 104 + )} 105 + </Tooltip> 106 + )} 66 107 </Flex> 67 108 68 109 {tagline != null && ( ··· 76 117 justify={Flex.Justify.END} 77 118 > 78 119 {ext.state === ExtensionState.NotDownloaded ? ( 79 - <Button 80 - color={Button.Colors.BRAND} 81 - submitting={busy} 82 - disabled={conflicting} 83 - onClick={() => { 84 - MoonbaseSettingsStore.installExtension(uniqueId); 85 - }} 120 + <Tooltip 121 + text={COMPAT_TEXT_MAP[ext.compat]} 122 + shouldShow={ext.compat !== ExtensionCompat.Compatible} 86 123 > 87 - Install 88 - </Button> 124 + {(props: any) => ( 125 + <Button 126 + {...props} 127 + color={Button.Colors.BRAND} 128 + submitting={busy} 129 + disabled={ 130 + ext.compat !== ExtensionCompat.Compatible || conflicting 131 + } 132 + onClick={async () => { 133 + await installWithDependencyPopup(uniqueId); 134 + }} 135 + > 136 + Install 137 + </Button> 138 + )} 139 + </Tooltip> 89 140 ) : ( 90 141 <div 91 142 // too lazy to learn how <Flex /> works lmao ··· 127 178 /> 128 179 )} 129 180 130 - <Switch 131 - checked={enabled} 181 + <FormSwitch 182 + value={ 183 + ext.compat === ExtensionCompat.Compatible && 184 + (enabled || implicitlyEnabled) 185 + } 186 + disabled={ 187 + implicitlyEnabled || ext.compat !== ExtensionCompat.Compatible 188 + } 189 + hideBorder={true} 190 + style={{ marginBottom: "0px" }} 191 + tooltipNote={ 192 + ext.compat !== ExtensionCompat.Compatible 193 + ? COMPAT_TEXT_MAP[ext.compat] 194 + : implicitlyEnabled 195 + ? `This extension is a dependency of the following enabled extension${ 196 + enabledDependants.length > 1 ? "s" : "" 197 + }: ${enabledDependants 198 + .map((a) => a.manifest.meta?.name ?? a.id) 199 + .join(", ")}` 200 + : undefined 201 + } 132 202 onChange={() => { 133 203 setRestartNeeded(true); 134 204 MoonbaseSettingsStore.setExtensionEnabled(uniqueId, !enabled); ··· 186 256 > 187 257 {tab === ExtensionPage.Info && <ExtensionInfo ext={ext} />} 188 258 {tab === ExtensionPage.Description && ( 189 - <Text variant="text-md/normal"> 190 - {MarkupUtils.parse(description ?? "*No description*")} 259 + <Text 260 + variant="text-md/normal" 261 + class={MarkupClasses.markup} 262 + style={{ width: "100%" }} 263 + > 264 + {MarkupUtils.parse(description ?? "*No description*", true, { 265 + allowHeading: true, 266 + allowLinks: true, 267 + allowList: true 268 + })} 191 269 </Text> 192 270 )} 193 271 {tab === ExtensionPage.Settings && <Settings ext={ext} />}
+9 -2
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/filterBar.tsx
··· 24 24 Enabled = 1 << 3, 25 25 Disabled = 1 << 4, 26 26 Installed = 1 << 5, 27 - Repository = 1 << 6 27 + Repository = 1 << 6, 28 + Incompatible = 1 << 7 28 29 } 29 - export const defaultFilter = ~(~0 << 7); 30 + export const defaultFilter = 127 as Filter; 30 31 31 32 const Margins = spacepack.findByCode("marginCenterHorz:")[0].exports; 32 33 const SortMenuClasses = spacepack.findByCode("container:", "clearText:")[0] ··· 130 131 /> 131 132 </MenuGroup> 132 133 <MenuGroup> 134 + <MenuCheckboxItem 135 + id="l-incompatible" 136 + label="Show incompatible" 137 + checked={filter & Filter.Incompatible} 138 + action={() => toggleFilter(Filter.Incompatible)} 139 + /> 133 140 <MenuItem 134 141 id="reset-all" 135 142 className={SortMenuClasses.clearText}
+21 -9
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/index.tsx
··· 8 8 import { useStateFromStoresObject } from "@moonlight-mod/wp/discord/packages/flux"; 9 9 10 10 import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 11 + import { ExtensionCompat } from "@moonlight-mod/core/extension/loader"; 11 12 12 13 const SearchBar: any = Object.values( 13 14 spacepack.findByCode("Messages.SEARCH", "hideSearchIcon")[0].exports 14 15 )[0]; 15 16 16 17 export default function ExtensionsPage() { 17 - const moonbaseId = MoonbaseSettingsStore.getExtensionUniqueId("moonbase")!; 18 18 const { extensions, savedFilter } = useStateFromStoresObject( 19 19 [MoonbaseSettingsStore], 20 20 () => { 21 21 return { 22 22 extensions: MoonbaseSettingsStore.extensions, 23 - savedFilter: MoonbaseSettingsStore.getExtensionConfig( 24 - moonbaseId, 25 - "filter" 23 + savedFilter: MoonbaseSettingsStore.getExtensionConfigRaw<number>( 24 + "moonbase", 25 + "filter", 26 + defaultFilter 26 27 ) 27 28 }; 28 29 } ··· 31 32 const [query, setQuery] = React.useState(""); 32 33 33 34 let filter: Filter, setFilter: (filter: Filter) => void; 34 - if (moonlight.getConfigOption<boolean>("moonbase", "saveFilter")) { 35 + if ( 36 + MoonbaseSettingsStore.getExtensionConfigRaw<boolean>( 37 + "moonbase", 38 + "saveFilter", 39 + false 40 + ) 41 + ) { 35 42 filter = savedFilter ?? defaultFilter; 36 43 setFilter = (filter) => 37 - MoonbaseSettingsStore.setExtensionConfig(moonbaseId, "filter", filter); 44 + MoonbaseSettingsStore.setExtensionConfig("moonbase", "filter", filter); 38 45 } else { 39 46 const state = React.useState(defaultFilter); 40 47 filter = state[0]; ··· 49 56 50 57 const filtered = sorted.filter( 51 58 (ext) => 52 - (ext.manifest.meta?.name?.toLowerCase().includes(query) || 59 + (query === "" || 60 + ext.manifest.id?.toLowerCase().includes(query) || 61 + ext.manifest.meta?.name?.toLowerCase().includes(query) || 53 62 ext.manifest.meta?.tagline?.toLowerCase().includes(query) || 54 63 ext.manifest.meta?.description?.toLowerCase().includes(query)) && 55 64 [...selectedTags.values()].every( ··· 71 80 ext.state !== ExtensionState.NotDownloaded) || 72 81 (!(filter & Filter.Repository) && 73 82 ext.state === ExtensionState.NotDownloaded) 74 - ) 83 + ) && 84 + (filter & Filter.Incompatible || 85 + ext.compat === ExtensionCompat.Compatible || 86 + (ext.compat === ExtensionCompat.InvalidApiLevel && ext.hasUpdate)) 75 87 ); 76 88 77 89 return ( ··· 96 108 setSelectedTags={setSelectedTags} 97 109 /> 98 110 {filtered.map((ext) => ( 99 - <ExtensionCard uniqueId={ext.uniqueId} key={ext.id} /> 111 + <ExtensionCard uniqueId={ext.uniqueId} key={ext.uniqueId} /> 100 112 ))} 101 113 </> 102 114 );
+2 -5
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/info.tsx
··· 178 178 [DependencyType.Incompatible]: "var(--red-400)" 179 179 }; 180 180 const color = colors[dep.type]; 181 - const id = MoonbaseSettingsStore.getExtensionUniqueId(dep.id); 182 - const name = 183 - (id !== null 184 - ? MoonbaseSettingsStore.getExtensionName(id!) 185 - : null) ?? dep.id; 181 + const name = MoonbaseSettingsStore.tryGetExtensionName(dep.id); 182 + 186 183 return ( 187 184 <Badge color={color} key={dep.id}> 188 185 {name}
+178
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/popup.tsx
··· 1 + // TODO: clean up the styling here 2 + import spacepack from "@moonlight-mod/wp/spacepack_spacepack"; 3 + import React from "@moonlight-mod/wp/react"; 4 + import { MoonbaseExtension } from "core-extensions/src/moonbase/types"; 5 + import * as Components from "@moonlight-mod/wp/discord/components/common/index"; 6 + import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 7 + import { ExtensionLoadSource } from "@moonlight-mod/types"; 8 + import Flex from "@moonlight-mod/wp/discord/uikit/Flex"; 9 + 10 + const { 11 + openModalLazy, 12 + closeModal 13 + } = require("@moonlight-mod/wp/discord/components/common/index"); 14 + const Popup = spacepack.findByCode(".minorContainer", "secondaryAction")[0] 15 + .exports.default; 16 + 17 + const presentableLoadSources: Record<ExtensionLoadSource, string> = { 18 + [ExtensionLoadSource.Developer]: "Local extension", // should never show up 19 + [ExtensionLoadSource.Core]: "Core extension", 20 + [ExtensionLoadSource.Normal]: "Extension repository" 21 + }; 22 + 23 + function ExtensionSelect({ 24 + id, 25 + candidates, 26 + option, 27 + setOption 28 + }: { 29 + id: string; 30 + candidates: MoonbaseExtension[]; 31 + option: string | undefined; 32 + setOption: (pick: string | undefined) => void; 33 + }) { 34 + const { SingleSelect } = Components; 35 + 36 + return ( 37 + <SingleSelect 38 + key={id} 39 + autofocus={false} 40 + value={option} 41 + options={candidates.map((candidate) => { 42 + return { 43 + value: candidate.uniqueId.toString(), 44 + label: 45 + candidate.source.url ?? 46 + presentableLoadSources[candidate.source.type] ?? 47 + candidate.manifest.version ?? 48 + "" 49 + }; 50 + })} 51 + onChange={(value: string) => { 52 + setOption(value); 53 + }} 54 + // @ts-expect-error no thanks 55 + placeholder="Missing extension" 56 + /> 57 + ); 58 + } 59 + 60 + function OurPopup({ 61 + deps, 62 + transitionState, 63 + id 64 + }: { 65 + deps: Record<string, MoonbaseExtension[]>; 66 + transitionState: number | null; 67 + id: string; 68 + }) { 69 + const { Text } = Components; 70 + 71 + const amountNotAvailable = Object.values(deps).filter( 72 + (candidates) => candidates.length === 0 73 + ).length; 74 + 75 + const [options, setOptions] = React.useState< 76 + Record<string, string | undefined> 77 + >( 78 + Object.fromEntries( 79 + Object.entries(deps).map(([id, candidates]) => [ 80 + id, 81 + candidates.length > 0 ? candidates[0].uniqueId.toString() : undefined 82 + ]) 83 + ) 84 + ); 85 + 86 + return ( 87 + <Popup 88 + body={ 89 + <Flex 90 + style={{ 91 + gap: "20px" 92 + }} 93 + direction={Flex.Direction.VERTICAL} 94 + > 95 + <Text variant="text-md/normal"> 96 + This extension depends on other extensions which are not downloaded. 97 + Choose which extensions to download. 98 + </Text> 99 + 100 + {amountNotAvailable > 0 && ( 101 + <Text variant="text-md/normal"> 102 + {amountNotAvailable} extension 103 + {amountNotAvailable > 1 ? "s" : ""} could not be found, and must 104 + be installed manually. 105 + </Text> 106 + )} 107 + 108 + <div 109 + style={{ 110 + display: "grid", 111 + gridTemplateColumns: "1fr 2fr", 112 + gap: "10px" 113 + }} 114 + > 115 + {Object.entries(deps).map(([id, candidates], i) => ( 116 + <> 117 + <Text 118 + variant="text-md/normal" 119 + style={{ 120 + alignSelf: "center", 121 + wordBreak: "break-word" 122 + }} 123 + > 124 + {MoonbaseSettingsStore.tryGetExtensionName(id)} 125 + </Text> 126 + 127 + <ExtensionSelect 128 + id={id} 129 + candidates={candidates} 130 + option={options[id]} 131 + setOption={(pick) => 132 + setOptions((prev) => ({ 133 + ...prev, 134 + [id]: pick 135 + })) 136 + } 137 + /> 138 + </> 139 + ))} 140 + </div> 141 + </Flex> 142 + } 143 + cancelText="Cancel" 144 + confirmText="Install" 145 + onCancel={() => { 146 + closeModal(id); 147 + }} 148 + onConfirm={() => { 149 + closeModal(id); 150 + 151 + for (const pick of Object.values(options)) { 152 + if (pick != null) { 153 + MoonbaseSettingsStore.installExtension(parseInt(pick)); 154 + } 155 + } 156 + }} 157 + title="Extension dependencies" 158 + transitionState={transitionState} 159 + /> 160 + ); 161 + } 162 + 163 + export async function doPopup(deps: Record<string, MoonbaseExtension[]>) { 164 + const id: string = await openModalLazy(async () => { 165 + // eslint-disable-next-line react/display-name 166 + return ({ transitionState }: { transitionState: number | null }) => { 167 + return <OurPopup transitionState={transitionState} deps={deps} id={id} />; 168 + }; 169 + }); 170 + } 171 + 172 + export default async function installWithDependencyPopup(uniqueId: number) { 173 + await MoonbaseSettingsStore.installExtension(uniqueId); 174 + const deps = await MoonbaseSettingsStore.getDependencies(uniqueId); 175 + if (deps != null) { 176 + await doPopup(deps); 177 + } 178 + }
+43 -9
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/settings.tsx
··· 60 60 hideBorder={true} 61 61 disabled={disabled} 62 62 onChange={(value: boolean) => { 63 - MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value); 63 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value); 64 64 }} 65 65 note={description} 66 66 className={`${Margins.marginReset} ${Margins.marginTop20}`} ··· 91 91 maxValue={castedSetting.max ?? 100} 92 92 onValueChange={(value: number) => { 93 93 const rounded = Math.max(min, Math.min(max, Math.round(value))); 94 - MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, rounded); 94 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, rounded); 95 95 }} 96 96 /> 97 97 </FormItem> ··· 114 114 value={value ?? ""} 115 115 onChange={(value: string) => { 116 116 if (disabled) return; 117 - MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value); 117 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value); 118 118 }} 119 119 /> 120 120 </FormItem> ··· 139 139 className={"moonbase-resizeable"} 140 140 onChange={(value: string) => { 141 141 if (disabled) return; 142 - MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value); 142 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value); 143 143 }} 144 144 /> 145 145 </FormItem> ··· 170 170 )} 171 171 onChange={(value: string) => { 172 172 if (disabled) return; 173 - MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, value); 173 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value); 174 174 }} 175 175 /> 176 176 </FormItem> ··· 206 206 onChange: (value: string) => { 207 207 if (disabled) return; 208 208 MoonbaseSettingsStore.setExtensionConfig( 209 - ext.uniqueId, 209 + ext.id, 210 210 name, 211 211 Array.from(value) 212 212 ); ··· 257 257 258 258 const entries = value ?? []; 259 259 const updateConfig = () => 260 - MoonbaseSettingsStore.setExtensionConfig(ext.uniqueId, name, entries); 260 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, entries); 261 261 262 262 return ( 263 263 <FormItem className={Margins.marginTop20} title={displayName}> ··· 323 323 const entries = Object.entries(value ?? {}); 324 324 const updateConfig = () => 325 325 MoonbaseSettingsStore.setExtensionConfig( 326 - ext.uniqueId, 326 + ext.id, 327 327 name, 328 328 Object.fromEntries(entries) 329 329 ); ··· 392 392 ); 393 393 } 394 394 395 + function Custom({ ext, name, setting, disabled }: SettingsProps) { 396 + const { value, displayName } = useConfigEntry<any>(ext.uniqueId, name); 397 + 398 + const { component: Component } = useStateFromStores( 399 + [MoonbaseSettingsStore], 400 + () => { 401 + return { 402 + component: MoonbaseSettingsStore.getExtensionConfigComponent( 403 + ext.id, 404 + name 405 + ) 406 + }; 407 + }, 408 + [ext.uniqueId, name] 409 + ); 410 + 411 + if (Component == null) { 412 + const { Text } = Components; 413 + return ( 414 + <Text variant="text/md/normal">{`Custom setting "${displayName}" is missing a component. Perhaps the extension is not installed?`}</Text> 415 + ); 416 + } 417 + 418 + return ( 419 + <Component 420 + value={value} 421 + setValue={(value) => 422 + MoonbaseSettingsStore.setExtensionConfig(ext.id, name, value) 423 + } 424 + /> 425 + ); 426 + } 427 + 395 428 function Setting({ ext, name, setting, disabled }: SettingsProps) { 396 429 const elements: Partial<Record<ExtensionSettingType, SettingsComponent>> = { 397 430 [ExtensionSettingType.Boolean]: Boolean, ··· 401 434 [ExtensionSettingType.Select]: Select, 402 435 [ExtensionSettingType.MultiSelect]: MultiSelect, 403 436 [ExtensionSettingType.List]: List, 404 - [ExtensionSettingType.Dictionary]: Dictionary 437 + [ExtensionSettingType.Dictionary]: Dictionary, 438 + [ExtensionSettingType.Custom]: Custom 405 439 }; 406 440 const element = elements[setting.type]; 407 441 if (element == null) return <></>;
+3
packages/core-extensions/src/moonbase/webpackModules/ui/index.tsx
··· 9 9 10 10 import ExtensionsPage from "./extensions"; 11 11 import ConfigPage from "./config"; 12 + import Update from "./update"; 12 13 13 14 const { Divider } = spacepack.findByCode(".forumOrHome]:")[0].exports.Z; 14 15 const TitleBarClasses = spacepack.findByCode("iconWrapper:", "children:")[0] ··· 81 82 ))} 82 83 </TabBar> 83 84 </div> 85 + 86 + <Update /> 84 87 85 88 {React.createElement(pages[subsection].element)} 86 89 </>
+87
packages/core-extensions/src/moonbase/webpackModules/ui/update.tsx
··· 1 + import { useStateFromStores } from "@moonlight-mod/wp/discord/packages/flux"; 2 + import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 3 + import * as Components from "@moonlight-mod/wp/discord/components/common/index"; 4 + import React from "@moonlight-mod/wp/react"; 5 + import spacepack from "@moonlight-mod/wp/spacepack_spacepack"; 6 + import Flex from "@moonlight-mod/wp/discord/uikit/Flex"; 7 + 8 + enum UpdateState { 9 + Ready, 10 + Working, 11 + Installed, 12 + Failed 13 + } 14 + 15 + const { ThemeDarkIcon, Text, Button } = Components; 16 + const Margins = spacepack.require("discord/styles/shared/Margins.css"); 17 + const HelpMessageClasses = spacepack.findByExports("positive", "iconDiv")[0] 18 + .exports; 19 + 20 + const logger = moonlight.getLogger("moonbase/ui/update"); 21 + 22 + const strings: Record<UpdateState, string> = { 23 + [UpdateState.Ready]: "A new version of moonlight is available.", 24 + [UpdateState.Working]: "Updating moonlight...", 25 + [UpdateState.Installed]: "Updated. Restart Discord to apply changes.", 26 + [UpdateState.Failed]: 27 + "Failed to update moonlight. Please use the installer instead." 28 + }; 29 + 30 + export default function Update() { 31 + const [state, setState] = React.useState(UpdateState.Ready); 32 + const newVersion = useStateFromStores( 33 + [MoonbaseSettingsStore], 34 + () => MoonbaseSettingsStore.newVersion 35 + ); 36 + 37 + if (newVersion == null) return null; 38 + 39 + // reimpl of HelpMessage but with a custom icon 40 + return ( 41 + <div 42 + className={`${Margins.marginBottom20} ${HelpMessageClasses.info} ${HelpMessageClasses.container} moonbase-update-section`} 43 + > 44 + <Flex direction={Flex.Direction.HORIZONTAL}> 45 + <div 46 + className={HelpMessageClasses.iconDiv} 47 + style={{ 48 + alignItems: "center" 49 + }} 50 + > 51 + <ThemeDarkIcon 52 + size="sm" 53 + color="currentColor" 54 + className={HelpMessageClasses.icon} 55 + /> 56 + </div> 57 + 58 + <Text 59 + variant="text-sm/medium" 60 + color="currentColor" 61 + className={HelpMessageClasses.text} 62 + > 63 + {strings[state]} 64 + </Text> 65 + </Flex> 66 + 67 + <Button 68 + look={Button.Looks.OUTLINED} 69 + color={Button.Colors.CUSTOM} 70 + size={Button.Sizes.TINY} 71 + disabled={state !== UpdateState.Ready} 72 + onClick={() => { 73 + setState(UpdateState.Working); 74 + 75 + MoonbaseSettingsStore.updateMoonlight() 76 + .then(() => setState(UpdateState.Installed)) 77 + .catch((e) => { 78 + logger.error(e); 79 + setState(UpdateState.Failed); 80 + }); 81 + }} 82 + > 83 + Update 84 + </Button> 85 + </div> 86 + ); 87 + }
+107
packages/core-extensions/src/moonbase/webpackModules/updates.tsx
··· 1 + import spacepack from "@moonlight-mod/wp/spacepack_spacepack"; 2 + import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores"; 3 + import Notices from "@moonlight-mod/wp/notices_notices"; 4 + import { MoonlightBranch } from "@moonlight-mod/types"; 5 + import React from "@moonlight-mod/wp/react"; 6 + import * as Components from "@moonlight-mod/wp/discord/components/common/index"; 7 + 8 + // FIXME: not indexed as importable 9 + const Constants = spacepack.require("discord/Constants"); 10 + const UserSettingsSections = spacepack.findObjectFromKey( 11 + Constants, 12 + "APPEARANCE_THEME_PICKER" 13 + ); 14 + 15 + const { ThemeDarkIcon } = Components; 16 + 17 + function plural(str: string, num: number) { 18 + return `${str}${num > 1 ? "s" : ""}`; 19 + } 20 + 21 + function listener() { 22 + if ( 23 + MoonbaseSettingsStore.shouldShowNotice && 24 + MoonbaseSettingsStore.getExtensionConfigRaw( 25 + "moonbase", 26 + "updateBanner", 27 + true 28 + ) 29 + ) { 30 + // @ts-expect-error epic type fail 31 + MoonbaseSettingsStore.removeChangeListener(listener); 32 + 33 + const version = MoonbaseSettingsStore.newVersion; 34 + const extensionUpdateCount = Object.keys( 35 + MoonbaseSettingsStore.updates 36 + ).length; 37 + const hasExtensionUpdates = extensionUpdateCount > 0; 38 + 39 + let message; 40 + 41 + if (version != null) { 42 + message = 43 + moonlightNode.branch === MoonlightBranch.NIGHTLY 44 + ? `A new version of moonlight is available` 45 + : `moonlight ${version} is available`; 46 + } 47 + 48 + if (hasExtensionUpdates) { 49 + let concat = false; 50 + if (message == null) { 51 + message = ""; 52 + } else { 53 + concat = true; 54 + message += ", and "; 55 + } 56 + message += `${extensionUpdateCount} ${concat ? "" : "moonlight "}${plural( 57 + "extension", 58 + extensionUpdateCount 59 + )} can be updated`; 60 + } 61 + 62 + if (message != null) message += "."; 63 + 64 + Notices.addNotice({ 65 + element: ( 66 + <div className="moonbase-updates-notice_text-wrapper"> 67 + <ThemeDarkIcon size="sm" color="currentColor" /> 68 + {message} 69 + </div> 70 + ), 71 + color: "moonbase-updates-notice", 72 + buttons: [ 73 + { 74 + name: "Open Moonbase", 75 + onClick: () => { 76 + const { open } = spacepack.findByExports( 77 + "setSection", 78 + "clearSubsection" 79 + )[0].exports.Z; 80 + 81 + // settings is lazy loaded thus lazily patched 82 + // FIXME: figure out a way to detect if settings has been opened 83 + // alreadyjust so the transition isnt as jarring 84 + open(UserSettingsSections.ACCOUNT); 85 + setTimeout(() => { 86 + if ( 87 + MoonbaseSettingsStore.getExtensionConfigRaw<boolean>( 88 + "moonbase", 89 + "sections", 90 + false 91 + ) 92 + ) { 93 + open("moonbase-extensions"); 94 + } else { 95 + open("moonbase", 0); 96 + } 97 + }, 0); 98 + return true; 99 + } 100 + } 101 + ] 102 + }); 103 + } 104 + } 105 + 106 + // @ts-expect-error epic type fail 107 + MoonbaseSettingsStore.addChangeListener(listener);
+79
packages/core-extensions/src/nativeFixes/host.ts
··· 1 + import { app, nativeTheme } from "electron"; 2 + 3 + const enabledFeatures = app.commandLine 4 + .getSwitchValue("enable-features") 5 + .split(","); 6 + 7 + moonlightHost.events.on("window-created", function (browserWindow) { 8 + if ( 9 + moonlightHost.getConfigOption<boolean>("nativeFixes", "devtoolsThemeFix") ?? 10 + true 11 + ) { 12 + browserWindow.webContents.on("devtools-opened", () => { 13 + if (!nativeTheme.shouldUseDarkColors) return; 14 + nativeTheme.themeSource = "light"; 15 + setTimeout(() => { 16 + nativeTheme.themeSource = "dark"; 17 + }, 100); 18 + }); 19 + } 20 + }); 21 + 22 + if ( 23 + moonlightHost.getConfigOption<boolean>( 24 + "nativeFixes", 25 + "disableRendererBackgrounding" 26 + ) ?? 27 + true 28 + ) { 29 + // Discord already disables UseEcoQoSForBackgroundProcess and some other 30 + // related features 31 + app.commandLine.appendSwitch("disable-renderer-backgrounding"); 32 + app.commandLine.appendSwitch("disable-backgrounding-occluded-windows"); 33 + 34 + // already added on Windows, but not on other operating systems 35 + app.commandLine.appendSwitch("disable-background-timer-throttling"); 36 + } 37 + 38 + if (process.platform === "linux") { 39 + if ( 40 + moonlightHost.getConfigOption<boolean>("nativeFixes", "linuxAutoscroll") ?? 41 + false 42 + ) { 43 + app.commandLine.appendSwitch( 44 + "enable-blink-features", 45 + "MiddleClickAutoscroll" 46 + ); 47 + } 48 + 49 + if ( 50 + moonlightHost.getConfigOption<boolean>( 51 + "nativeFixes", 52 + "linuxSpeechDispatcher" 53 + ) ?? 54 + true 55 + ) { 56 + app.commandLine.appendSwitch("enable-speech-dispatcher"); 57 + } 58 + } 59 + 60 + // NOTE: Only tested if this appears on Windows, it should appear on all when 61 + // hardware acceleration is disabled 62 + const noAccel = app.commandLine.hasSwitch("disable-gpu-compositing"); 63 + if ( 64 + (moonlightHost.getConfigOption<boolean>("nativeFixes", "vaapi") ?? true) && 65 + !noAccel 66 + ) { 67 + if (process.platform === "linux") 68 + // These will eventually be renamed https://source.chromium.org/chromium/chromium/src/+/5482210941a94d70406b8da962426e4faca7fce4 69 + enabledFeatures.push( 70 + "VaapiVideoEncoder", 71 + "VaapiVideoDecoder", 72 + "VaapiVideoDecodeLinuxGL" 73 + ); 74 + } 75 + 76 + app.commandLine.appendSwitch( 77 + "enable-features", 78 + [...new Set(enabledFeatures)].join(",") 79 + );
+42
packages/core-extensions/src/nativeFixes/manifest.json
··· 1 + { 2 + "id": "nativeFixes", 3 + "meta": { 4 + "name": "Native Fixes", 5 + "tagline": "Various configurable fixes for Discord and Electron", 6 + "authors": ["Cynosphere", "adryd"], 7 + "tags": ["fixes"] 8 + }, 9 + "settings": { 10 + "devtoolsThemeFix": { 11 + "displayName": "Devtools Theme Fix", 12 + "description": "Temporary workaround for devtools defaulting to light theme on Electron 32", 13 + "type": "boolean", 14 + "default": true 15 + }, 16 + "disableRendererBackgrounding": { 17 + "displayName": "Disable Renderer Backgrounding", 18 + "description": "This is enabled by default as a power saving measure, but it breaks screensharing and websocket connections fairly often", 19 + "type": "boolean", 20 + "default": true 21 + }, 22 + "linuxAutoscroll": { 23 + "displayName": "Enable middle click autoscroll on Linux", 24 + "description": "Requires manual configuration of your system to disable middle click paste, has no effect on other operating systems", 25 + "type": "boolean", 26 + "default": false 27 + }, 28 + "linuxSpeechDispatcher": { 29 + "displayName": "Enable speech-dispatcher for TTS on Linux", 30 + "description": "Fixes text-to-speech. Has no effect on other operating systems", 31 + "type": "boolean", 32 + "default": true 33 + }, 34 + "vaapi": { 35 + "displayName": "Enable VAAPI features on Linux", 36 + "description": "Provides hardware accelerated video encode and decode. Has no effect on other operating systems", 37 + "type": "boolean", 38 + "default": true 39 + } 40 + }, 41 + "apiLevel": 2 42 + }
+1 -1
packages/core-extensions/src/noHideToken/index.ts
··· 1 - import { Patch } from "types/src"; 1 + import { Patch } from "@moonlight-mod/types"; 2 2 3 3 export const patches: Patch[] = [ 4 4 {
+2 -1
packages/core-extensions/src/noHideToken/manifest.json
··· 3 3 "apiLevel": 2, 4 4 "meta": { 5 5 "name": "No Hide Token", 6 - "tagline": "Disables removal of token from localStorage when opening dev tools", 6 + "tagline": "Prevents you from being logged-out on hard-crash", 7 + "description": "Prevents you from being logged-out on hard-crash by disabling removal of token from localStorage when opening dev tools", 7 8 "authors": ["adryd"], 8 9 "tags": ["dangerZone", "development"] 9 10 }
-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 - });
+5 -1
packages/core-extensions/src/noTrack/manifest.json
··· 6 6 "tagline": "Disables /api/science and analytics", 7 7 "authors": ["Cynosphere", "NotNite"], 8 8 "tags": ["privacy"] 9 - } 9 + }, 10 + "blocked": [ 11 + "https://*.discord.com/api/v*/science", 12 + "https://*.discord.com/api/v*/metrics" 13 + ] 10 14 }
+46
packages/core-extensions/src/notices/index.ts
··· 1 + import type { ExtensionWebpackModule, Patch } from "@moonlight-mod/types"; 2 + 3 + export const patches: Patch[] = [ 4 + { 5 + find: ".GUILD_RAID_NOTIFICATION:", 6 + replace: { 7 + match: 8 + /(?<=return(\(0,.\.jsx\))\(.+?\);)case .{1,2}\..{1,3}\.GUILD_RAID_NOTIFICATION:/, 9 + replacement: (orig, createElement) => 10 + `case "__moonlight_notice":return${createElement}(require("notices_component").default,{});${orig}` 11 + } 12 + }, 13 + { 14 + find: '"NoticeStore"', 15 + replace: [ 16 + { 17 + match: /\[.{1,2}\..{1,3}\.CONNECT_SPOTIFY\]:{/, 18 + replacement: (orig: string) => 19 + `__moonlight_notice:{predicate:()=>require("notices_notices").default.shouldShowNotice()},${orig}` 20 + }, 21 + { 22 + match: /=\[(.{1,2}\..{1,3}\.QUARANTINED,)/g, 23 + replacement: (_, orig) => `=["__moonlight_notice",${orig}` 24 + } 25 + ] 26 + } 27 + ]; 28 + 29 + export const webpackModules: Record<string, ExtensionWebpackModule> = { 30 + notices: { 31 + dependencies: [ 32 + { id: "discord/packages/flux" }, 33 + { id: "discord/Dispatcher" } 34 + ] 35 + }, 36 + 37 + component: { 38 + dependencies: [ 39 + { id: "react" }, 40 + { id: "discord/Dispatcher" }, 41 + { id: "discord/components/common/index" }, 42 + { id: "discord/packages/flux" }, 43 + { ext: "notices", id: "notices" } 44 + ] 45 + } 46 + };
+10
packages/core-extensions/src/notices/manifest.json
··· 1 + { 2 + "id": "notices", 3 + "apiLevel": 2, 4 + "meta": { 5 + "name": "Notices", 6 + "tagline": "An API for adding notices at the top of the page", 7 + "authors": ["Cynosphere", "NotNite"], 8 + "tags": ["library"] 9 + } 10 + }
+56
packages/core-extensions/src/notices/webpackModules/component.tsx
··· 1 + import React from "@moonlight-mod/wp/react"; 2 + import Dispatcher from "@moonlight-mod/wp/discord/Dispatcher"; 3 + import * as Components from "@moonlight-mod/wp/discord/components/common/index"; 4 + import { useStateFromStoresObject } from "@moonlight-mod/wp/discord/packages/flux"; 5 + import NoticesStore from "@moonlight-mod/wp/notices_notices"; 6 + import type { Notice } from "@moonlight-mod/types/coreExtensions/notices"; 7 + 8 + // FIXME: types 9 + const { Notice, NoticeCloseButton, PrimaryCTANoticeButton } = Components; 10 + 11 + function popAndDismiss(notice: Notice) { 12 + NoticesStore.popNotice(); 13 + if (notice?.onDismiss) { 14 + notice.onDismiss(); 15 + } 16 + if (!NoticesStore.shouldShowNotice()) { 17 + Dispatcher.dispatch({ 18 + type: "NOTICE_DISMISS" 19 + }); 20 + } 21 + } 22 + 23 + export default function UpdateNotice() { 24 + const { notice } = useStateFromStoresObject([NoticesStore], () => ({ 25 + notice: NoticesStore.getCurrentNotice() 26 + })); 27 + 28 + if (notice == null) return <></>; 29 + 30 + return ( 31 + <Notice color={notice.color}> 32 + {notice.element} 33 + 34 + {(notice.showClose ?? true) && ( 35 + <NoticeCloseButton 36 + onClick={() => popAndDismiss(notice)} 37 + noticeType="__moonlight_notice" 38 + /> 39 + )} 40 + 41 + {(notice.buttons ?? []).map((button) => ( 42 + <PrimaryCTANoticeButton 43 + key={button.name} 44 + onClick={() => { 45 + if (button.onClick()) { 46 + popAndDismiss(notice); 47 + } 48 + }} 49 + noticeType="__moonlight_notice" 50 + > 51 + {button.name} 52 + </PrimaryCTANoticeButton> 53 + ))} 54 + </Notice> 55 + ); 56 + }
+58
packages/core-extensions/src/notices/webpackModules/notices.ts
··· 1 + import { Store } from "@moonlight-mod/wp/discord/packages/flux"; 2 + import Dispatcher from "@moonlight-mod/wp/discord/Dispatcher"; 3 + import type { 4 + Notice, 5 + Notices 6 + } from "@moonlight-mod/types/coreExtensions/notices"; 7 + 8 + // very lazy way of doing this, FIXME 9 + let open = false; 10 + 11 + class NoticesStore extends Store<any> { 12 + private notices: Notice[] = []; 13 + 14 + constructor() { 15 + super(Dispatcher); 16 + } 17 + 18 + addNotice(notice: Notice) { 19 + this.notices.push(notice); 20 + if (open && this.notices.length !== 0) { 21 + Dispatcher.dispatch({ 22 + type: "NOTICE_SHOW", 23 + notice: { type: "__moonlight_notice" } 24 + }); 25 + } 26 + this.emitChange(); 27 + } 28 + 29 + popNotice() { 30 + this.notices.shift(); 31 + this.emitChange(); 32 + } 33 + 34 + getCurrentNotice() { 35 + return this.notices.length > 0 ? this.notices[0] : null; 36 + } 37 + 38 + shouldShowNotice() { 39 + return this.notices.length > 0; 40 + } 41 + } 42 + 43 + const store: Notices = new NoticesStore(); 44 + 45 + function showNotice() { 46 + open = true; 47 + if (store.shouldShowNotice()) { 48 + Dispatcher.dispatch({ 49 + type: "NOTICE_SHOW", 50 + notice: { type: "__moonlight_notice" } 51 + }); 52 + } 53 + } 54 + 55 + Dispatcher.subscribe("CONNECTION_OPEN", showNotice); 56 + Dispatcher.subscribe("CONNECTION_OPEN_SUPPLEMENTAL", showNotice); 57 + 58 + export default store;
+3 -3
packages/core-extensions/src/quietLoggers/index.ts
··· 8 8 // that end up causing syntax errors by the normal patch 9 9 const loggerFixes: Patch[] = [ 10 10 { 11 - find: '"./ggsans-800-extrabolditalic.woff2":', 11 + find: '"./gg-sans/ggsans-800-extrabolditalic.woff2":', 12 12 replace: { 13 - match: /throw .+?,./, 14 - replacement: "return{}" 13 + match: /var .=Error.+?;throw .+?,./, 14 + replacement: "" 15 15 } 16 16 }, 17 17 {
+2
packages/core-extensions/src/rocketship/host.ts
··· 1 + import "./host/permissions"; 2 + import "./host/venmic";
+96
packages/core-extensions/src/rocketship/host/permissions.ts
··· 1 + import type { BrowserWindow } from "electron"; 2 + 3 + type PermissionRequestHandler = ( 4 + webContents: Electron.WebContents, 5 + permission: string, 6 + callback: (permissionGranted: boolean) => void, 7 + details: Electron.PermissionRequestHandlerHandlerDetails 8 + ) => void; 9 + 10 + type PermissionCheckHandler = ( 11 + webContents: Electron.WebContents | null, 12 + permission: string, 13 + requestingOrigin: string, 14 + details: Electron.PermissionCheckHandlerHandlerDetails 15 + ) => boolean; 16 + 17 + moonlightHost.events.on( 18 + "window-created", 19 + (window: BrowserWindow, isMainWindow: boolean) => { 20 + if (!isMainWindow) return; 21 + const windowSession = window.webContents.session; 22 + 23 + // setPermissionRequestHandler 24 + windowSession.setPermissionRequestHandler( 25 + (webcontents, permission, callback, details) => { 26 + let cbResult = false; 27 + function fakeCallback(result: boolean) { 28 + cbResult = result; 29 + } 30 + 31 + if (caughtPermissionRequestHandler) { 32 + caughtPermissionRequestHandler( 33 + webcontents, 34 + permission, 35 + fakeCallback, 36 + details 37 + ); 38 + } 39 + 40 + if (permission === "media" || permission === "display-capture") { 41 + cbResult = true; 42 + } 43 + 44 + callback(cbResult); 45 + } 46 + ); 47 + 48 + let caughtPermissionRequestHandler: PermissionRequestHandler | undefined; 49 + 50 + windowSession.setPermissionRequestHandler = 51 + function catchSetPermissionRequestHandler( 52 + handler: ( 53 + webcontents: Electron.WebContents, 54 + permission: string, 55 + callback: (permissionGranted: boolean) => void 56 + ) => void 57 + ) { 58 + caughtPermissionRequestHandler = handler; 59 + }; 60 + 61 + // setPermissionCheckHandler 62 + windowSession.setPermissionCheckHandler( 63 + (webcontents, permission, requestingOrigin, details) => { 64 + return false; 65 + } 66 + ); 67 + 68 + let caughtPermissionCheckHandler: PermissionCheckHandler | undefined; 69 + 70 + windowSession.setPermissionCheckHandler( 71 + (webcontents, permission, requestingOrigin, details) => { 72 + let result = false; 73 + 74 + if (caughtPermissionCheckHandler) { 75 + result = caughtPermissionCheckHandler( 76 + webcontents, 77 + permission, 78 + requestingOrigin, 79 + details 80 + ); 81 + } 82 + 83 + if (permission === "media" || permission === "display-capture") { 84 + result = true; 85 + } 86 + 87 + return result; 88 + } 89 + ); 90 + 91 + windowSession.setPermissionCheckHandler = 92 + function catchSetPermissionCheckHandler(handler: PermissionCheckHandler) { 93 + caughtPermissionCheckHandler = handler; 94 + }; 95 + } 96 + );
+35
packages/core-extensions/src/rocketship/host/types.ts
··· 1 + // https://github.com/Vencord/venmic/blob/d737ef33eaae7a73d03ec02673e008cf0243434d/lib/module.d.ts 2 + type DefaultProps = "node.name" | "application.name"; 3 + 4 + type LiteralUnion<LiteralType, BaseType extends string> = 5 + | LiteralType 6 + | (BaseType & Record<never, never>); 7 + 8 + type Optional<Type, Key extends keyof Type> = Partial<Pick<Type, Key>> & 9 + Omit<Type, Key>; 10 + 11 + export type Node<T extends string = never> = Record< 12 + LiteralUnion<T, string>, 13 + string 14 + >; 15 + 16 + export interface LinkData { 17 + include: Node[]; 18 + exclude: Node[]; 19 + 20 + ignore_devices?: boolean; 21 + 22 + only_speakers?: boolean; 23 + only_default_speakers?: boolean; 24 + 25 + workaround?: Node[]; 26 + } 27 + 28 + export interface PatchBay { 29 + unlink(): void; 30 + 31 + list<T extends string = DefaultProps>(props?: T[]): Node<T>[]; 32 + link( 33 + data: Optional<LinkData, "exclude"> | Optional<LinkData, "include"> 34 + ): boolean; 35 + }
+77
packages/core-extensions/src/rocketship/host/venmic.ts
··· 1 + import type { BrowserWindow } from "electron"; 2 + import { app, desktopCapturer } from "electron"; 3 + import path from "node:path"; 4 + import { type PatchBay } from "./types"; 5 + 6 + const logger = moonlightHost.getLogger("rocketship"); 7 + 8 + function getPatchbay() { 9 + try { 10 + const venmic = require( 11 + path.join(path.dirname(moonlightHost.asarPath), "..", "venmic.node") 12 + ) as { PatchBay: new () => PatchBay }; 13 + const patchbay = new venmic.PatchBay(); 14 + return patchbay; 15 + } catch (error) { 16 + logger.error("Failed to load venmic.node:", error); 17 + return null; 18 + } 19 + } 20 + 21 + const patchbay = getPatchbay(); 22 + 23 + // TODO: figure out how to map source to window with venmic 24 + function linkVenmic() { 25 + if (patchbay == null) return false; 26 + 27 + try { 28 + const pid = 29 + app 30 + .getAppMetrics() 31 + .find((proc) => proc.name === "Audio Service") 32 + ?.pid?.toString() ?? ""; 33 + 34 + logger.info("Audio Service PID:", pid); 35 + 36 + patchbay.unlink(); 37 + return patchbay.link({ 38 + exclude: [ 39 + { "application.process.id": pid }, 40 + { "media.class": "Stream/Input/Audio" } 41 + ], 42 + ignore_devices: true, 43 + only_speakers: true, 44 + only_default_speakers: true 45 + }); 46 + } catch (error) { 47 + logger.error("Failed to link venmic:", error); 48 + return false; 49 + } 50 + } 51 + 52 + moonlightHost.events.on( 53 + "window-created", 54 + (window: BrowserWindow, isMainWindow: boolean) => { 55 + if (!isMainWindow) return; 56 + const windowSession = window.webContents.session; 57 + 58 + // @ts-expect-error these types ancient 59 + windowSession.setDisplayMediaRequestHandler( 60 + (request: any, callback: any) => { 61 + const linked = linkVenmic(); 62 + desktopCapturer 63 + .getSources({ types: ["screen", "window"] }) 64 + .then((sources) => { 65 + //logger.debug("desktopCapturer.getSources", sources); 66 + logger.debug("Linked to venmic:", linked); 67 + 68 + callback({ 69 + video: sources[0], 70 + audio: "loopback" 71 + }); 72 + }); 73 + }, 74 + { useSystemPicker: true } 75 + ); 76 + } 77 + );
+130
packages/core-extensions/src/rocketship/index.ts
··· 1 + import { Patch } from "@moonlight-mod/types"; 2 + 3 + const logger = moonlight.getLogger("rocketship"); 4 + const getDisplayMediaOrig = navigator.mediaDevices.getDisplayMedia; 5 + 6 + async function getVenmicStream() { 7 + try { 8 + const devices = await navigator.mediaDevices.enumerateDevices(); 9 + logger.debug("Devices:", devices); 10 + 11 + // This isn't vencord :( 12 + const id = devices.find((device) => device.label === "vencord-screen-share") 13 + ?.deviceId; 14 + if (!id) return null; 15 + logger.debug("Got venmic device ID:", id); 16 + 17 + const stream = await navigator.mediaDevices.getUserMedia({ 18 + audio: { 19 + deviceId: { 20 + exact: id 21 + }, 22 + autoGainControl: false, 23 + echoCancellation: false, 24 + noiseSuppression: false 25 + } 26 + }); 27 + 28 + return stream.getAudioTracks(); 29 + } catch (error) { 30 + logger.warn("Failed to get venmic stream:", error); 31 + return null; 32 + } 33 + } 34 + 35 + navigator.mediaDevices.getDisplayMedia = async function getDisplayMediaRedirect( 36 + options 37 + ) { 38 + const orig = await getDisplayMediaOrig.call(this, options); 39 + 40 + const venmic = await getVenmicStream(); 41 + logger.debug("venmic", venmic); 42 + if (venmic != null) { 43 + // venmic will be proxying all audio, so we need to remove the original 44 + // tracks to not cause overlap 45 + for (const track of orig.getAudioTracks()) { 46 + orig.removeTrack(track); 47 + } 48 + 49 + for (const track of venmic) { 50 + orig.addTrack(track); 51 + } 52 + } 53 + 54 + return orig; 55 + }; 56 + 57 + export const patches: Patch[] = [ 58 + // "Ensure discord_voice is happy" 59 + { 60 + find: "RustAudioDeviceModule", 61 + replace: [ 62 + { 63 + match: /static supported\(\)\{.+?\}/, 64 + replacement: "static supported(){return true}" 65 + }, 66 + { 67 + match: "supported(){return!0}", 68 + replacement: "supported(){return true}" 69 + } 70 + ] 71 + }, 72 + // Remove Native media engine from list of choices 73 + { 74 + find: '.CAMERA_BACKGROUND_LIVE="cameraBackgroundLive"', 75 + replace: { 76 + match: /.\..{1,2}\.NATIVE,/, 77 + replacement: "" 78 + } 79 + }, 80 + // Stub out browser checks to allow us to use WebRTC voice on Embedded 81 + { 82 + find: "Using Unified Plan (", 83 + replace: { 84 + match: /return .\..{1,2}\?\((.)\.info/, 85 + replacement: (_, logger) => `return true?(${logger}.info` 86 + } 87 + }, 88 + { 89 + find: '"UnifiedConnection("', 90 + replace: { 91 + match: /this\.videoSupported=.\..{1,2};/, 92 + replacement: "this.videoSupported=true;" 93 + } 94 + }, 95 + { 96 + find: "OculusBrowser", 97 + replace: [ 98 + { 99 + match: /"Firefox"===(.)\(\)\.name/g, 100 + replacement: (orig, info) => `true||${orig}` 101 + } 102 + ] 103 + }, 104 + { 105 + find: ".getMediaEngine().getDesktopSource", 106 + replace: { 107 + match: /.\.isPlatformEmbedded/, 108 + replacement: "false" 109 + } 110 + }, 111 + { 112 + // Matching MediaEngineStore 113 + find: '"displayName","MediaEngineStore")', 114 + replace: [ 115 + // Prevent loading of krisp native module by stubbing out desktop checks 116 + { 117 + match: 118 + /\(\(0,.\.isWindows\)\(\)\|\|\(0,.\.isLinux\)\(\)\|\|.+?&&!__OVERLAY__/, 119 + replacement: (orig, macosPlatformCheck) => `false&&!__OVERLAY__` 120 + }, 121 + // Enable loading of web krisp equivelant by replacing isWeb with true 122 + { 123 + match: 124 + /\(0,.\.isWeb\)\(\)&&(.{1,2}\.supports\(.{1,2}\..{1,2}.NOISE_CANCELLATION)/, 125 + replacement: (orig, supportsNoiseCancellation) => 126 + `true&&${supportsNoiseCancellation}` 127 + } 128 + ] 129 + } 130 + ];
+10
packages/core-extensions/src/rocketship/manifest.json
··· 1 + { 2 + "id": "rocketship", 3 + "apiLevel": 2, 4 + "meta": { 5 + "name": "Rocketship", 6 + "tagline": "Adds new features when using rocketship", 7 + "description": "**This extension only works on Linux when using rocketship:**\nhttps://github.com/moonlight-mod/rocketship\n\nAdds new features to the Discord Linux client with rocketship, such as a better screensharing experience.", 8 + "authors": ["NotNite", "Cynosphere", "adryd"] 9 + } 10 + }
+3 -3
packages/core-extensions/src/settings/index.ts
··· 5 5 { 6 6 find: '"useGenerateUserSettingsSections"', 7 7 replace: { 8 - match: /(?<=\.push\(.+?\)}\)\)}\),)./, 9 - replacement: (sections: string) => 10 - `require("settings_settings").Settings._mutateSections(${sections})` 8 + match: /(?<=\.push\(.+?\)}\)\)}\),)(.+?)}/, 9 + replacement: (_, sections: string) => 10 + `require("settings_settings").Settings._mutateSections(${sections})}` 11 11 } 12 12 }, 13 13 {
+57
packages/core/src/asar.ts
··· 1 + // https://github.com/electron/asar 2 + // http://formats.kaitai.io/python_pickle/ 3 + import { BinaryReader } from "./util/binary"; 4 + 5 + /* 6 + The asar format is kinda bad, especially because it uses multiple pickle 7 + entries. It spams sizes, expecting us to read small buffers and parse those, 8 + but we can just take it all through at once without having to create multiple 9 + BinaryReaders. This implementation might be wrong, though. 10 + 11 + This either has size/offset or files but I can't get the type to cooperate, 12 + so pretend this is a union. 13 + */ 14 + 15 + type AsarEntry = { 16 + size: number; 17 + offset: `${number}`; // who designed this 18 + 19 + files?: Record<string, AsarEntry>; 20 + }; 21 + 22 + export default function extractAsar(file: ArrayBuffer) { 23 + const array = new Uint8Array(file); 24 + const br = new BinaryReader(array); 25 + 26 + // two uints, one containing the number '4', to signify that the other uint takes up 4 bytes 27 + // bravo, electron, bravo 28 + const _payloadSize = br.readUInt32(); 29 + const _headerSize = br.readInt32(); 30 + 31 + const headerStringStart = br.position; 32 + const headerStringSize = br.readUInt32(); // How big the block is 33 + const actualStringSize = br.readUInt32(); // How big the string in that block is 34 + 35 + const base = headerStringStart + headerStringSize + 4; 36 + 37 + const string = br.readString(actualStringSize); 38 + const header: AsarEntry = JSON.parse(string); 39 + 40 + const ret: Record<string, Uint8Array> = {}; 41 + function addDirectory(dir: AsarEntry, path: string) { 42 + for (const [name, data] of Object.entries(dir.files!)) { 43 + const fullName = path + "/" + name; 44 + if (data.files != null) { 45 + addDirectory(data, fullName); 46 + } else { 47 + br.position = base + parseInt(data.offset); 48 + const file = br.read(data.size); 49 + ret[fullName] = file; 50 + } 51 + } 52 + } 53 + 54 + addDirectory(header, ""); 55 + 56 + return ret; 57 + }
+35 -31
packages/core/src/config.ts
··· 1 1 import { Config } from "@moonlight-mod/types"; 2 - import requireImport from "./util/import"; 3 2 import { getConfigPath } from "./util/data"; 3 + import * as constants from "@moonlight-mod/types/constants"; 4 + import Logger from "./util/logger"; 5 + 6 + const logger = new Logger("core/config"); 4 7 5 8 const defaultConfig: Config = { 6 9 extensions: { ··· 9 12 noTrack: true, 10 13 noHideToken: true 11 14 }, 12 - repositories: ["https://moonlight-mod.github.io/extensions-dist/repo.json"] 15 + repositories: [constants.mainRepo] 13 16 }; 14 17 15 - export function writeConfig(config: Config) { 16 - const fs = requireImport("fs"); 17 - const configPath = getConfigPath(); 18 - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 19 - } 20 - 21 - function readConfigNode(): Config { 22 - const fs = requireImport("fs"); 23 - const configPath = getConfigPath(); 24 - 25 - if (!fs.existsSync(configPath)) { 26 - writeConfig(defaultConfig); 27 - return defaultConfig; 18 + export async function writeConfig(config: Config) { 19 + try { 20 + const configPath = await getConfigPath(); 21 + await moonlightFS.writeFileString( 22 + configPath, 23 + JSON.stringify(config, null, 2) 24 + ); 25 + } catch (e) { 26 + logger.error("Failed to write config", e); 28 27 } 29 - 30 - let config: Config = JSON.parse(fs.readFileSync(configPath, "utf8")); 31 - 32 - // Assign the default values if they don't exist (newly added) 33 - config = { ...defaultConfig, ...config }; 34 - writeConfig(config); 35 - 36 - return config; 37 28 } 38 29 39 - export function readConfig(): Config { 30 + export async function readConfig(): Promise<Config> { 40 31 webPreload: { 41 32 return moonlightNode.config; 42 33 } 43 34 44 - nodePreload: { 45 - return readConfigNode(); 46 - } 35 + const configPath = await getConfigPath(); 36 + if (!(await moonlightFS.exists(configPath))) { 37 + await writeConfig(defaultConfig); 38 + return defaultConfig; 39 + } else { 40 + try { 41 + let config: Config = JSON.parse( 42 + await moonlightFS.readFileString(configPath) 43 + ); 44 + // Assign the default values if they don't exist (newly added) 45 + config = { ...defaultConfig, ...config }; 46 + await writeConfig(config); 47 47 48 - injector: { 49 - return readConfigNode(); 48 + return config; 49 + } catch (e) { 50 + logger.error("Failed to read config, falling back to defaults", e); 51 + // We don't want to write the default config here - if a user is manually 52 + // editing their config and messes it up, we'll delete it all instead of 53 + // letting them fix it 54 + return defaultConfig; 55 + } 50 56 } 51 - 52 - throw new Error("Called readConfig() in an impossible environment"); 53 57 }
+140 -78
packages/core/src/extension.ts
··· 5 5 constants 6 6 } from "@moonlight-mod/types"; 7 7 import { readConfig } from "./config"; 8 - import requireImport from "./util/import"; 9 8 import { getCoreExtensionsPath, getExtensionsPath } from "./util/data"; 9 + import Logger from "./util/logger"; 10 + 11 + const logger = new Logger("core/extension"); 10 12 11 - function findManifests(dir: string): string[] { 12 - const fs = requireImport("fs"); 13 - const path = requireImport("path"); 13 + async function findManifests(dir: string): Promise<string[]> { 14 14 const ret = []; 15 15 16 - if (fs.existsSync(dir)) { 17 - for (const file of fs.readdirSync(dir)) { 16 + if (await moonlightFS.exists(dir)) { 17 + for (const file of await moonlightFS.readdir(dir)) { 18 + const path = moonlightFS.join(dir, file); 18 19 if (file === "manifest.json") { 19 - ret.push(path.join(dir, file)); 20 + ret.push(path); 20 21 } 21 22 22 - if (fs.statSync(path.join(dir, file)).isDirectory()) { 23 - ret.push(...findManifests(path.join(dir, file))); 23 + if (!(await moonlightFS.isFile(path))) { 24 + ret.push(...(await findManifests(path))); 24 25 } 25 26 } 26 27 } ··· 28 29 return ret; 29 30 } 30 31 31 - function loadDetectedExtensions( 32 + async function loadDetectedExtensions( 32 33 dir: string, 33 34 type: ExtensionLoadSource 34 - ): DetectedExtension[] { 35 - const fs = requireImport("fs"); 36 - const path = requireImport("path"); 35 + ): Promise<DetectedExtension[]> { 37 36 const ret: DetectedExtension[] = []; 38 37 39 - const manifests = findManifests(dir); 38 + const manifests = await findManifests(dir); 40 39 for (const manifestPath of manifests) { 41 - if (!fs.existsSync(manifestPath)) continue; 42 - const dir = path.dirname(manifestPath); 40 + try { 41 + if (!(await moonlightFS.exists(manifestPath))) continue; 42 + const dir = moonlightFS.dirname(manifestPath); 43 43 44 - const manifest: ExtensionManifest = JSON.parse( 45 - fs.readFileSync(manifestPath, "utf8") 46 - ); 47 - const level = manifest.apiLevel ?? 1; 48 - if (level !== constants.apiLevel) { 49 - continue; 50 - } 44 + const manifest: ExtensionManifest = JSON.parse( 45 + await moonlightFS.readFileString(manifestPath) 46 + ); 51 47 52 - const webPath = path.join(dir, "index.js"); 53 - const nodePath = path.join(dir, "node.js"); 54 - const hostPath = path.join(dir, "host.js"); 48 + const webPath = moonlightFS.join(dir, "index.js"); 49 + const nodePath = moonlightFS.join(dir, "node.js"); 50 + const hostPath = moonlightFS.join(dir, "host.js"); 55 51 56 - // if none exist (empty manifest) don't give a shit 57 - if ( 58 - !fs.existsSync(webPath) && 59 - !fs.existsSync(nodePath) && 60 - !fs.existsSync(hostPath) 61 - ) { 62 - continue; 63 - } 52 + // if none exist (empty manifest) don't give a shit 53 + if ( 54 + !moonlightFS.exists(webPath) && 55 + !moonlightFS.exists(nodePath) && 56 + !moonlightFS.exists(hostPath) 57 + ) { 58 + continue; 59 + } 64 60 65 - const web = fs.existsSync(webPath) 66 - ? fs.readFileSync(webPath, "utf8") 67 - : undefined; 61 + const web = (await moonlightFS.exists(webPath)) 62 + ? await moonlightFS.readFileString(webPath) 63 + : undefined; 68 64 69 - let url: string | undefined = undefined; 70 - const urlPath = path.join(dir, constants.repoUrlFile); 71 - if (type === ExtensionLoadSource.Normal && fs.existsSync(urlPath)) { 72 - url = fs.readFileSync(urlPath, "utf8"); 73 - } 65 + let url: string | undefined = undefined; 66 + const urlPath = moonlightFS.join(dir, constants.repoUrlFile); 67 + if ( 68 + type === ExtensionLoadSource.Normal && 69 + (await moonlightFS.exists(urlPath)) 70 + ) { 71 + url = await moonlightFS.readFileString(urlPath); 72 + } 74 73 75 - const wpModules: Record<string, string> = {}; 76 - const wpModulesPath = path.join(dir, "webpackModules"); 77 - if (fs.existsSync(wpModulesPath)) { 78 - const wpModulesFile = fs.readdirSync(wpModulesPath); 74 + const wpModules: Record<string, string> = {}; 75 + const wpModulesPath = moonlightFS.join(dir, "webpackModules"); 76 + if (await moonlightFS.exists(wpModulesPath)) { 77 + const wpModulesFile = await moonlightFS.readdir(wpModulesPath); 79 78 80 - for (const wpModuleFile of wpModulesFile) { 81 - if (wpModuleFile.endsWith(".js")) { 82 - wpModules[wpModuleFile.replace(".js", "")] = fs.readFileSync( 83 - path.join(wpModulesPath, wpModuleFile), 84 - "utf8" 85 - ); 79 + for (const wpModuleFile of wpModulesFile) { 80 + if (wpModuleFile.endsWith(".js")) { 81 + wpModules[wpModuleFile.replace(".js", "")] = 82 + await moonlightFS.readFileString( 83 + moonlightFS.join(wpModulesPath, wpModuleFile) 84 + ); 85 + } 86 86 } 87 87 } 88 - } 89 88 90 - ret.push({ 91 - id: manifest.id, 92 - manifest, 93 - source: { 94 - type, 95 - url 96 - }, 97 - scripts: { 98 - web, 99 - webPath: web != null ? webPath : undefined, 100 - webpackModules: wpModules, 101 - nodePath: fs.existsSync(nodePath) ? nodePath : undefined, 102 - hostPath: fs.existsSync(hostPath) ? hostPath : undefined 103 - } 104 - }); 89 + ret.push({ 90 + id: manifest.id, 91 + manifest, 92 + source: { 93 + type, 94 + url 95 + }, 96 + scripts: { 97 + web, 98 + webPath: web != null ? webPath : undefined, 99 + webpackModules: wpModules, 100 + nodePath: (await moonlightFS.exists(nodePath)) ? nodePath : undefined, 101 + hostPath: (await moonlightFS.exists(hostPath)) ? hostPath : undefined 102 + } 103 + }); 104 + } catch (e) { 105 + logger.error(e, "Failed to load extension"); 106 + } 105 107 } 106 108 107 109 return ret; 108 110 } 109 111 110 - function getExtensionsNative(): DetectedExtension[] { 111 - const config = readConfig(); 112 + async function getExtensionsNative(): Promise<DetectedExtension[]> { 113 + const config = await readConfig(); 112 114 const res = []; 113 115 114 116 res.push( 115 - ...loadDetectedExtensions(getCoreExtensionsPath(), ExtensionLoadSource.Core) 117 + ...(await loadDetectedExtensions( 118 + getCoreExtensionsPath(), 119 + ExtensionLoadSource.Core 120 + )) 116 121 ); 117 122 118 123 res.push( 119 - ...loadDetectedExtensions(getExtensionsPath(), ExtensionLoadSource.Normal) 124 + ...(await loadDetectedExtensions( 125 + await getExtensionsPath(), 126 + ExtensionLoadSource.Normal 127 + )) 120 128 ); 121 129 122 130 for (const devSearchPath of config.devSearchPaths ?? []) { 123 131 res.push( 124 - ...loadDetectedExtensions(devSearchPath, ExtensionLoadSource.Developer) 132 + ...(await loadDetectedExtensions( 133 + devSearchPath, 134 + ExtensionLoadSource.Developer 135 + )) 125 136 ); 126 137 } 127 138 128 139 return res; 129 140 } 130 141 131 - export function getExtensions(): DetectedExtension[] { 142 + async function getExtensionsBrowser(): Promise<DetectedExtension[]> { 143 + const ret: DetectedExtension[] = []; 144 + 145 + const coreExtensionsFs: Record<string, string> = JSON.parse( 146 + // @ts-expect-error shut up 147 + _moonlight_coreExtensionsStr 148 + ); 149 + const coreExtensions = Array.from( 150 + new Set(Object.keys(coreExtensionsFs).map((x) => x.split("/")[0])) 151 + ); 152 + 153 + for (const ext of coreExtensions) { 154 + if (!coreExtensionsFs[`${ext}/index.js`]) continue; 155 + const manifest = JSON.parse(coreExtensionsFs[`${ext}/manifest.json`]); 156 + const web = coreExtensionsFs[`${ext}/index.js`]; 157 + 158 + const wpModules: Record<string, string> = {}; 159 + const wpModulesPath = `${ext}/webpackModules`; 160 + for (const wpModuleFile of Object.keys(coreExtensionsFs)) { 161 + if (wpModuleFile.startsWith(wpModulesPath)) { 162 + wpModules[ 163 + wpModuleFile.replace(wpModulesPath + "/", "").replace(".js", "") 164 + ] = coreExtensionsFs[wpModuleFile]; 165 + } 166 + } 167 + 168 + ret.push({ 169 + id: manifest.id, 170 + manifest, 171 + source: { 172 + type: ExtensionLoadSource.Core 173 + }, 174 + scripts: { 175 + web, 176 + webpackModules: wpModules 177 + } 178 + }); 179 + } 180 + 181 + if (await moonlightFS.exists("/extensions")) { 182 + ret.push( 183 + ...(await loadDetectedExtensions( 184 + "/extensions", 185 + ExtensionLoadSource.Normal 186 + )) 187 + ); 188 + } 189 + 190 + return ret; 191 + } 192 + 193 + export async function getExtensions(): Promise<DetectedExtension[]> { 132 194 webPreload: { 133 195 return moonlightNode.extensions; 134 196 } 135 197 136 - nodePreload: { 137 - return getExtensionsNative(); 198 + browser: { 199 + return await getExtensionsBrowser(); 138 200 } 139 201 140 - injector: { 141 - return getExtensionsNative(); 202 + nodeTarget: { 203 + return await getExtensionsNative(); 142 204 } 143 205 144 206 throw new Error("Called getExtensions() outside of node-preload/web-preload");
+91 -53
packages/core/src/extension/loader.ts
··· 2 2 ExtensionWebExports, 3 3 DetectedExtension, 4 4 ProcessedExtensions, 5 - WebpackModuleFunc 5 + WebpackModuleFunc, 6 + constants, 7 + ExtensionManifest, 8 + ExtensionEnvironment 6 9 } from "@moonlight-mod/types"; 7 10 import { readConfig } from "../config"; 8 11 import Logger from "../util/logger"; ··· 14 17 15 18 const logger = new Logger("core/extension/loader"); 16 19 17 - async function loadExt(ext: DetectedExtension) { 18 - webPreload: { 19 - if (ext.scripts.web != null) { 20 - const source = ext.scripts.web; 21 - const fn = new Function("require", "module", "exports", source); 20 + function loadExtWeb(ext: DetectedExtension) { 21 + if (ext.scripts.web != null) { 22 + const source = ext.scripts.web; 23 + const fn = new Function("require", "module", "exports", source); 22 24 23 - const module = { id: ext.id, exports: {} }; 24 - fn.apply(window, [ 25 - () => { 26 - logger.warn("Attempted to require() from web"); 27 - }, 28 - module, 29 - module.exports 30 - ]); 25 + const module = { id: ext.id, exports: {} }; 26 + fn.apply(window, [ 27 + () => { 28 + logger.warn("Attempted to require() from web"); 29 + }, 30 + module, 31 + module.exports 32 + ]); 31 33 32 - const exports: ExtensionWebExports = module.exports; 33 - if (exports.patches != null) { 34 - let idx = 0; 35 - for (const patch of exports.patches) { 36 - if (Array.isArray(patch.replace)) { 37 - for (const replacement of patch.replace) { 38 - const newPatch = Object.assign({}, patch, { 39 - replace: replacement 40 - }); 34 + const exports: ExtensionWebExports = module.exports; 35 + if (exports.patches != null) { 36 + let idx = 0; 37 + for (const patch of exports.patches) { 38 + if (Array.isArray(patch.replace)) { 39 + for (const replacement of patch.replace) { 40 + const newPatch = Object.assign({}, patch, { 41 + replace: replacement 42 + }); 41 43 42 - registerPatch({ ...newPatch, ext: ext.id, id: idx }); 43 - idx++; 44 - } 45 - } else { 46 - registerPatch({ ...patch, ext: ext.id, id: idx }); 44 + registerPatch({ ...newPatch, ext: ext.id, id: idx }); 47 45 idx++; 48 46 } 47 + } else { 48 + registerPatch({ ...patch, ext: ext.id, id: idx }); 49 + idx++; 49 50 } 50 51 } 52 + } 51 53 52 - if (exports.webpackModules != null) { 53 - for (const [name, wp] of Object.entries(exports.webpackModules)) { 54 - if (wp.run == null && ext.scripts.webpackModules?.[name] != null) { 55 - const func = new Function( 56 - "module", 57 - "exports", 58 - "require", 59 - ext.scripts.webpackModules[name]! 60 - ) as WebpackModuleFunc; 61 - registerWebpackModule({ 62 - ...wp, 63 - ext: ext.id, 64 - id: name, 65 - run: func 66 - }); 67 - } else { 68 - registerWebpackModule({ ...wp, ext: ext.id, id: name }); 69 - } 54 + if (exports.webpackModules != null) { 55 + for (const [name, wp] of Object.entries(exports.webpackModules)) { 56 + if (wp.run == null && ext.scripts.webpackModules?.[name] != null) { 57 + const func = new Function( 58 + "module", 59 + "exports", 60 + "require", 61 + ext.scripts.webpackModules[name]! 62 + ) as WebpackModuleFunc; 63 + registerWebpackModule({ 64 + ...wp, 65 + ext: ext.id, 66 + id: name, 67 + run: func 68 + }); 69 + } else { 70 + registerWebpackModule({ ...wp, ext: ext.id, id: name }); 70 71 } 71 72 } 73 + } 72 74 73 - if (exports.styles != null) { 74 - registerStyles( 75 - exports.styles.map((style, i) => `/* ${ext.id}#${i} */ ${style}`) 76 - ); 77 - } 75 + if (exports.styles != null) { 76 + registerStyles( 77 + exports.styles.map((style, i) => `/* ${ext.id}#${i} */ ${style}`) 78 + ); 78 79 } 79 80 } 81 + } 82 + 83 + async function loadExt(ext: DetectedExtension) { 84 + webTarget: { 85 + loadExtWeb(ext); 86 + } 80 87 81 88 nodePreload: { 82 89 if (ext.scripts.nodePath != null) { ··· 100 107 } 101 108 } 102 109 110 + export enum ExtensionCompat { 111 + Compatible, 112 + InvalidApiLevel, 113 + InvalidEnvironment 114 + } 115 + 116 + export function checkExtensionCompat( 117 + manifest: ExtensionManifest 118 + ): ExtensionCompat { 119 + let environment; 120 + webTarget: { 121 + environment = ExtensionEnvironment.Web; 122 + } 123 + nodeTarget: { 124 + environment = ExtensionEnvironment.Desktop; 125 + } 126 + 127 + if (manifest.apiLevel !== constants.apiLevel) 128 + return ExtensionCompat.InvalidApiLevel; 129 + if ( 130 + (manifest.environment ?? "both") !== "both" && 131 + manifest.environment !== environment 132 + ) 133 + return ExtensionCompat.InvalidEnvironment; 134 + return ExtensionCompat.Compatible; 135 + } 136 + 103 137 /* 104 138 This function resolves extensions and loads them, split into a few stages: 105 139 ··· 117 151 export async function loadExtensions( 118 152 exts: DetectedExtension[] 119 153 ): Promise<ProcessedExtensions> { 120 - const config = readConfig(); 154 + exts = exts.filter( 155 + (ext) => checkExtensionCompat(ext.manifest) === ExtensionCompat.Compatible 156 + ); 157 + 158 + const config = await readConfig(); 121 159 const items = exts 122 160 .map((ext) => { 123 161 return { ··· 205 243 logger.debug(`Loaded "${ext.id}"`); 206 244 } 207 245 208 - webPreload: { 246 + webTarget: { 209 247 for (const ext of extensions) { 210 248 moonlight.enabledExtensions.add(ext.id); 211 249 }
+50
packages/core/src/fs.ts
··· 1 + import type { MoonlightFS } from "@moonlight-mod/types"; 2 + import requireImport from "./util/import"; 3 + 4 + export default function createFS(): MoonlightFS { 5 + const fs = requireImport("fs"); 6 + const path = requireImport("path"); 7 + 8 + return { 9 + async readFile(path) { 10 + const file = fs.readFileSync(path); 11 + return new Uint8Array(file); 12 + }, 13 + async readFileString(path) { 14 + return fs.readFileSync(path, "utf8"); 15 + }, 16 + async writeFile(path, data) { 17 + fs.writeFileSync(path, Buffer.from(data)); 18 + }, 19 + async writeFileString(path, data) { 20 + fs.writeFileSync(path, data, "utf8"); 21 + }, 22 + async unlink(path) { 23 + fs.unlinkSync(path); 24 + }, 25 + 26 + async readdir(path) { 27 + return fs.readdirSync(path); 28 + }, 29 + async mkdir(path) { 30 + fs.mkdirSync(path, { recursive: true }); 31 + }, 32 + async rmdir(path) { 33 + fs.rmSync(path, { recursive: true }); 34 + }, 35 + 36 + async exists(path) { 37 + return fs.existsSync(path); 38 + }, 39 + async isFile(path) { 40 + return fs.statSync(path).isFile(); 41 + }, 42 + 43 + join(...parts) { 44 + return path.join(...parts); 45 + }, 46 + dirname(dir) { 47 + return path.dirname(dir); 48 + } 49 + }; 50 + }
+66
packages/core/src/util/binary.ts
··· 1 + // https://github.com/NotNite/brc-save-editor/blob/main/src/lib/binary.ts 2 + export interface BinaryInterface { 3 + data: Uint8Array; 4 + view: DataView; 5 + length: number; 6 + position: number; 7 + } 8 + 9 + export class BinaryReader implements BinaryInterface { 10 + data: Uint8Array; 11 + view: DataView; 12 + length: number; 13 + position: number; 14 + 15 + constructor(data: Uint8Array) { 16 + this.data = data; 17 + this.view = new DataView(data.buffer); 18 + 19 + this.length = data.length; 20 + this.position = 0; 21 + } 22 + 23 + readByte() { 24 + return this._read(this.view.getInt8, 1); 25 + } 26 + 27 + readBoolean() { 28 + return this.readByte() !== 0; 29 + } 30 + 31 + readInt32() { 32 + return this._read(this.view.getInt32, 4); 33 + } 34 + 35 + readUInt32() { 36 + return this._read(this.view.getUint32, 4); 37 + } 38 + 39 + readSingle() { 40 + return this._read(this.view.getFloat32, 4); 41 + } 42 + 43 + readInt64() { 44 + return this._read(this.view.getBigInt64, 8); 45 + } 46 + 47 + readString(length: number) { 48 + const result = this.read(length); 49 + return new TextDecoder().decode(result); 50 + } 51 + 52 + read(length: number) { 53 + const data = this.data.subarray(this.position, this.position + length); 54 + this.position += length; 55 + return data; 56 + } 57 + 58 + private _read<T>( 59 + func: (position: number, littleEndian?: boolean) => T, 60 + length: number 61 + ): T { 62 + const result = func.call(this.view, this.position, true); 63 + this.position += length; 64 + return result; 65 + } 66 + }
+28 -26
packages/core/src/util/data.ts
··· 1 1 import { constants } from "@moonlight-mod/types"; 2 - import requireImport from "./import"; 2 + 3 + export async function getMoonlightDir() { 4 + browser: { 5 + return "/"; 6 + } 3 7 4 - export function getMoonlightDir(): string { 5 8 const electron = require("electron"); 6 - const fs = requireImport("fs"); 7 - const path = requireImport("path"); 8 9 9 10 let appData = ""; 10 11 injector: { ··· 15 16 appData = electron.ipcRenderer.sendSync(constants.ipcGetAppData); 16 17 } 17 18 18 - const dir = path.join(appData, "moonlight-mod"); 19 - if (!fs.existsSync(dir)) fs.mkdirSync(dir); 19 + const dir = moonlightFS.join(appData, "moonlight-mod"); 20 + if (!(await moonlightFS.exists(dir))) await moonlightFS.mkdir(dir); 20 21 21 22 return dir; 22 23 } ··· 26 27 version: string; 27 28 }; 28 29 29 - export function getConfigPath(): string { 30 - const dir = getMoonlightDir(); 31 - const fs = requireImport("fs"); 32 - const path = requireImport("path"); 30 + export async function getConfigPath() { 31 + browser: { 32 + return "/config.json"; 33 + } 34 + 35 + const dir = await getMoonlightDir(); 33 36 34 37 let configPath = ""; 35 38 36 - const buildInfoPath = path.join(process.resourcesPath, "build_info.json"); 37 - if (!fs.existsSync(buildInfoPath)) { 38 - configPath = path.join(dir, "desktop.json"); 39 + const buildInfoPath = moonlightFS.join( 40 + process.resourcesPath, 41 + "build_info.json" 42 + ); 43 + if (!(await moonlightFS.exists(buildInfoPath))) { 44 + configPath = moonlightFS.join(dir, "desktop.json"); 39 45 } else { 40 46 const buildInfo: BuildInfo = JSON.parse( 41 - fs.readFileSync(buildInfoPath, "utf8") 47 + await moonlightFS.readFileString(buildInfoPath) 42 48 ); 43 - configPath = path.join(dir, buildInfo.releaseChannel + ".json"); 49 + configPath = moonlightFS.join(dir, buildInfo.releaseChannel + ".json"); 44 50 } 45 51 46 52 return configPath; 47 53 } 48 54 49 - function getPathFromMoonlight(...names: string[]): string { 50 - const dir = getMoonlightDir(); 51 - const fs = requireImport("fs"); 52 - const path = requireImport("path"); 55 + async function getPathFromMoonlight(...names: string[]) { 56 + const dir = await getMoonlightDir(); 53 57 54 - const target = path.join(dir, ...names); 55 - if (!fs.existsSync(target)) fs.mkdirSync(target); 58 + const target = moonlightFS.join(dir, ...names); 59 + if (!(await moonlightFS.exists(target))) await moonlightFS.mkdir(target); 56 60 57 61 return target; 58 62 } 59 63 60 - export function getExtensionsPath(): string { 61 - return getPathFromMoonlight(constants.extensionsDir); 64 + export async function getExtensionsPath() { 65 + return await getPathFromMoonlight(constants.extensionsDir); 62 66 } 63 67 64 68 export function getCoreExtensionsPath(): string { 65 - const path = requireImport("path"); 66 - const a = path.join(__dirname, constants.coreExtensionsDir); 67 - return a; 69 + return moonlightFS.join(__dirname, constants.coreExtensionsDir); 68 70 }
+73 -91
packages/core/src/util/event.ts
··· 1 1 import { MoonlightEventEmitter } from "@moonlight-mod/types/core/event"; 2 2 3 - function nodeMethod< 3 + export function createEventEmitter< 4 4 EventId extends string = string, 5 5 EventData = Record<EventId, any> 6 6 >(): MoonlightEventEmitter<EventId, EventData> { 7 - const EventEmitter = require("events"); 8 - const eventEmitter = new EventEmitter(); 9 - const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 7 + webTarget: { 8 + const eventEmitter = new EventTarget(); 9 + const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 10 10 11 - return { 12 - dispatchEvent: <Id extends keyof EventData>( 13 - id: Id, 14 - data: EventData[Id] 15 - ) => { 16 - eventEmitter.emit(id as string, data); 17 - }, 18 - 19 - addEventListener: <Id extends keyof EventData>( 20 - id: Id, 21 - cb: (data: EventData[Id]) => void 22 - ) => { 23 - const untyped = cb as (data: EventData) => void; 24 - if (listeners.has(untyped)) return; 25 - 26 - function listener(e: Event) { 27 - const event = e as CustomEvent<string>; 28 - cb(event as EventData[Id]); 29 - } 30 - 31 - listeners.set(untyped, listener); 32 - eventEmitter.on(id as string, listener); 33 - }, 34 - 35 - removeEventListener: <Id extends keyof EventData>( 36 - id: Id, 37 - cb: (data: EventData[Id]) => void 38 - ) => { 39 - const untyped = cb as (data: EventData) => void; 40 - const listener = listeners.get(untyped); 41 - if (listener == null) return; 42 - listeners.delete(untyped); 43 - eventEmitter.off(id as string, listener); 44 - } 45 - }; 46 - } 11 + return { 12 + dispatchEvent: <Id extends keyof EventData>( 13 + id: Id, 14 + data: EventData[Id] 15 + ) => { 16 + eventEmitter.dispatchEvent( 17 + new CustomEvent(id as string, { detail: data }) 18 + ); 19 + }, 47 20 48 - function webMethod< 49 - EventId extends string = string, 50 - EventData = Record<EventId, any> 51 - >(): MoonlightEventEmitter<EventId, EventData> { 52 - const eventEmitter = new EventTarget(); 53 - const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 21 + addEventListener: <Id extends keyof EventData>( 22 + id: Id, 23 + cb: (data: EventData[Id]) => void 24 + ) => { 25 + const untyped = cb as (data: EventData) => void; 26 + if (listeners.has(untyped)) return; 54 27 55 - return { 56 - dispatchEvent: <Id extends keyof EventData>( 57 - id: Id, 58 - data: EventData[Id] 59 - ) => { 60 - eventEmitter.dispatchEvent( 61 - new CustomEvent(id as string, { detail: data }) 62 - ); 63 - }, 28 + function listener(e: Event) { 29 + const event = e as CustomEvent<string>; 30 + cb(event.detail as EventData[Id]); 31 + } 64 32 65 - addEventListener: <Id extends keyof EventData>( 66 - id: Id, 67 - cb: (data: EventData[Id]) => void 68 - ) => { 69 - const untyped = cb as (data: EventData) => void; 70 - if (listeners.has(untyped)) return; 33 + listeners.set(untyped, listener); 34 + eventEmitter.addEventListener(id as string, listener); 35 + }, 71 36 72 - function listener(e: Event) { 73 - const event = e as CustomEvent<string>; 74 - cb(event.detail as EventData[Id]); 37 + removeEventListener: <Id extends keyof EventData>( 38 + id: Id, 39 + cb: (data: EventData[Id]) => void 40 + ) => { 41 + const untyped = cb as (data: EventData) => void; 42 + const listener = listeners.get(untyped); 43 + if (listener == null) return; 44 + listeners.delete(untyped); 45 + eventEmitter.removeEventListener(id as string, listener); 75 46 } 47 + }; 48 + } 76 49 77 - listeners.set(untyped, listener); 78 - eventEmitter.addEventListener(id as string, listener); 79 - }, 50 + nodeTarget: { 51 + const EventEmitter = require("events"); 52 + const eventEmitter = new EventEmitter(); 53 + const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); 80 54 81 - removeEventListener: <Id extends keyof EventData>( 82 - id: Id, 83 - cb: (data: EventData[Id]) => void 84 - ) => { 85 - const untyped = cb as (data: EventData) => void; 86 - const listener = listeners.get(untyped); 87 - if (listener == null) return; 88 - listeners.delete(untyped); 89 - eventEmitter.removeEventListener(id as string, listener); 90 - } 91 - }; 92 - } 55 + return { 56 + dispatchEvent: <Id extends keyof EventData>( 57 + id: Id, 58 + data: EventData[Id] 59 + ) => { 60 + eventEmitter.emit(id as string, data); 61 + }, 62 + 63 + addEventListener: <Id extends keyof EventData>( 64 + id: Id, 65 + cb: (data: EventData[Id]) => void 66 + ) => { 67 + const untyped = cb as (data: EventData) => void; 68 + if (listeners.has(untyped)) return; 93 69 94 - export function createEventEmitter< 95 - EventId extends string = string, 96 - EventData = Record<EventId, any> 97 - >(): MoonlightEventEmitter<EventId, EventData> { 98 - webPreload: { 99 - return webMethod(); 100 - } 70 + function listener(e: Event) { 71 + const event = e as CustomEvent<string>; 72 + cb(event as EventData[Id]); 73 + } 101 74 102 - nodePreload: { 103 - return nodeMethod(); 104 - } 75 + listeners.set(untyped, listener); 76 + eventEmitter.on(id as string, listener); 77 + }, 105 78 106 - injector: { 107 - return nodeMethod(); 79 + removeEventListener: <Id extends keyof EventData>( 80 + id: Id, 81 + cb: (data: EventData[Id]) => void 82 + ) => { 83 + const untyped = cb as (data: EventData) => void; 84 + const listener = listeners.get(untyped); 85 + if (listener == null) return; 86 + listeners.delete(untyped); 87 + eventEmitter.off(id as string, listener); 88 + } 89 + }; 108 90 } 109 91 110 92 throw new Error("Called createEventEmitter() in an impossible environment");
+12 -10
packages/core/src/util/logger.ts
··· 1 1 /* eslint-disable no-console */ 2 2 import { LogLevel } from "@moonlight-mod/types/logger"; 3 - import { readConfig } from "../config"; 3 + import { Config } from "@moonlight-mod/types"; 4 4 5 5 const colors = { 6 6 [LogLevel.SILLY]: "#EDD3E9", ··· 11 11 [LogLevel.ERROR]: "#FF0000" 12 12 }; 13 13 14 - const config = readConfig(); 15 14 let maxLevel = LogLevel.INFO; 16 - if (config.loggerLevel != null) { 17 - const enumValue = 18 - LogLevel[config.loggerLevel.toUpperCase() as keyof typeof LogLevel]; 19 - if (enumValue != null) { 20 - maxLevel = enumValue; 21 - } 22 - } 23 15 24 16 export default class Logger { 25 17 private name: string; ··· 57 49 const logLevel = LogLevel[level].toUpperCase(); 58 50 if (maxLevel > level) return; 59 51 60 - if (MOONLIGHT_WEB_PRELOAD) { 52 + if (MOONLIGHT_WEB_PRELOAD || MOONLIGHT_BROWSER) { 61 53 args = [ 62 54 `%c[${logLevel}]`, 63 55 `background-color: ${colors[level]}; color: #FFFFFF;`, ··· 92 84 } 93 85 } 94 86 } 87 + 88 + export function initLogger(config: Config) { 89 + if (config.loggerLevel != null) { 90 + const enumValue = 91 + LogLevel[config.loggerLevel.toUpperCase() as keyof typeof LogLevel]; 92 + if (enumValue != null) { 93 + maxLevel = enumValue; 94 + } 95 + } 96 + }
+57 -9
packages/injector/src/index.ts
··· 5 5 app 6 6 } from "electron"; 7 7 import Module from "node:module"; 8 - import { constants } from "@moonlight-mod/types"; 8 + import { constants, MoonlightBranch } from "@moonlight-mod/types"; 9 9 import { readConfig } from "@moonlight-mod/core/config"; 10 10 import { getExtensions } from "@moonlight-mod/core/extension"; 11 - import Logger from "@moonlight-mod/core/util/logger"; 11 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 12 12 import { 13 13 loadExtensions, 14 14 loadProcessedExtensions ··· 16 16 import EventEmitter from "node:events"; 17 17 import { join, resolve } from "node:path"; 18 18 import persist from "@moonlight-mod/core/persist"; 19 + import createFS from "@moonlight-mod/core/fs"; 19 20 20 21 const logger = new Logger("injector"); 21 22 22 23 let oldPreloadPath: string | undefined; 23 24 let corsAllow: string[] = []; 25 + let blockedUrls: RegExp[] = []; 24 26 let isMoonlightDesktop = false; 25 27 let hasOpenAsar = false; 26 28 let openAsarConfigPreload: string | undefined; ··· 41 43 corsAllow = list; 42 44 }); 43 45 46 + const reEscapeRegExp = /[\\^$.*+?()[\]{}|]/g; 47 + const reMatchPattern = 48 + /^(?<scheme>\*|[a-z][a-z0-9+.-]*):\/\/(?<host>.+?)\/(?<path>.+)?$/; 49 + 50 + const escapeRegExp = (s: string) => s.replace(reEscapeRegExp, "\\$&"); 51 + ipcMain.handle(constants.ipcSetBlockedList, (_, list: string[]) => { 52 + // We compile the patterns into a RegExp based on a janky match pattern-like syntax 53 + const compiled = list 54 + .map((pattern) => { 55 + const match = pattern.match(reMatchPattern); 56 + if (!match?.groups) return; 57 + 58 + let regex = ""; 59 + if (match.groups.scheme === "*") regex += ".+?"; 60 + else regex += escapeRegExp(match.groups.scheme); 61 + regex += ":\\/\\/"; 62 + 63 + const parts = match.groups.host.split("."); 64 + if (parts[0] === "*") { 65 + parts.shift(); 66 + regex += "(?:.+?\\.)?"; 67 + } 68 + regex += escapeRegExp(parts.join(".")); 69 + 70 + regex += "\\/" + escapeRegExp(match.groups.path).replace("\\*", ".*?"); 71 + 72 + return new RegExp("^" + regex + "$"); 73 + }) 74 + .filter(Boolean) as RegExp[]; 75 + 76 + blockedUrls = compiled; 77 + }); 78 + 44 79 function patchCsp(headers: Record<string, string[]>) { 45 80 const directives = [ 46 81 "style-src", ··· 89 124 constructor(opts: BrowserWindowConstructorOptions) { 90 125 oldPreloadPath = opts.webPreferences!.preload; 91 126 92 - // Only overwrite preload if its the actual main client window 93 - if (opts.webPreferences!.preload!.indexOf("discord_desktop_core") > -1) { 127 + const isMainWindow = 128 + opts.webPreferences!.preload!.indexOf("discord_desktop_core") > -1; 129 + 130 + if (isMainWindow) 94 131 opts.webPreferences!.preload = require.resolve("./node-preload.js"); 95 - } 96 132 97 133 // Event for modifying window options 98 - moonlightHost.events.emit("window-options", opts); 134 + moonlightHost.events.emit("window-options", opts, isMainWindow); 99 135 100 136 super(opts); 101 137 102 138 // Event for when a window is created 103 - moonlightHost.events.emit("window-created", this); 139 + moonlightHost.events.emit("window-created", this, isMainWindow); 104 140 105 141 this.webContents.session.webRequest.onHeadersReceived((details, cb) => { 106 142 if (details.responseHeaders != null) { ··· 118 154 } 119 155 }); 120 156 157 + // Allow plugins to block some URLs, 158 + // this is needed because multiple webRequest handlers cannot be registered at once 159 + this.webContents.session.webRequest.onBeforeRequest((details, cb) => { 160 + cb({ cancel: blockedUrls.some((u) => u.test(details.url)) }); 161 + }); 162 + 121 163 if (hasOpenAsar) { 122 164 // Remove DOM injections 123 165 // Settings can still be opened via: ··· 160 202 161 203 export async function inject(asarPath: string) { 162 204 isMoonlightDesktop = asarPath === "moonlightDesktop"; 205 + global.moonlightFS = createFS(); 206 + 163 207 try { 164 - const config = readConfig(); 165 - const extensions = getExtensions(); 208 + const config = await readConfig(); 209 + initLogger(config); 210 + const extensions = await getExtensions(); 166 211 167 212 // Duplicated in node-preload... oops 168 213 // eslint-disable-next-line no-inner-declarations ··· 181 226 extensions: [], 182 227 dependencyGraph: new Map() 183 228 }, 229 + 230 + version: MOONLIGHT_VERSION, 231 + branch: MOONLIGHT_BRANCH as MoonlightBranch, 184 232 185 233 getConfig, 186 234 getConfigOption: <T>(ext: string, name: string) => {
+37 -16
packages/node-preload/src/index.ts
··· 1 1 import { webFrame, ipcRenderer, contextBridge } from "electron"; 2 - import fs from "fs"; 3 - import path from "path"; 2 + import fs from "node:fs"; 3 + import path from "node:path"; 4 4 5 5 import { readConfig, writeConfig } from "@moonlight-mod/core/config"; 6 - import { constants } from "@moonlight-mod/types"; 6 + import { constants, MoonlightBranch } from "@moonlight-mod/types"; 7 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"; 8 + import { 9 + getExtensionsPath, 10 + getMoonlightDir 11 + } from "@moonlight-mod/core/util/data"; 12 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 10 13 import { 11 14 loadExtensions, 12 15 loadProcessedExtensions 13 16 } from "@moonlight-mod/core/extension/loader"; 17 + import createFS from "@moonlight-mod/core/fs"; 14 18 15 19 async function injectGlobals() { 16 - const config = readConfig(); 17 - const extensions = getExtensions(); 18 - const processed = await loadExtensions(extensions); 20 + global.moonlightFS = createFS(); 21 + 22 + const config = await readConfig(); 23 + initLogger(config); 24 + const extensions = await getExtensions(); 25 + const processedExtensions = await loadExtensions(extensions); 26 + const moonlightDir = await getMoonlightDir(); 27 + const extensionsPath = await getExtensionsPath(); 19 28 20 29 function getConfig(ext: string) { 21 30 const val = config.extensions[ext]; ··· 25 34 26 35 global.moonlightNode = { 27 36 config, 28 - extensions: getExtensions(), 29 - processedExtensions: processed, 37 + extensions, 38 + processedExtensions, 30 39 nativesCache: {}, 40 + isBrowser: false, 41 + 42 + version: MOONLIGHT_VERSION, 43 + branch: MOONLIGHT_BRANCH as MoonlightBranch, 44 + 31 45 getConfig, 32 46 getConfigOption: <T>(ext: string, name: string) => { 33 47 const config = getConfig(ext); ··· 41 55 return new Logger(id); 42 56 }, 43 57 58 + getMoonlightDir() { 59 + return moonlightDir; 60 + }, 44 61 getExtensionDir: (ext: string) => { 45 - const extPath = getExtensionsPath(); 46 - return path.join(extPath, ext); 62 + return path.join(extensionsPath, ext); 47 63 }, 48 64 writeConfig 49 65 }; 50 66 51 - await loadProcessedExtensions(processed); 67 + await loadProcessedExtensions(processedExtensions); 52 68 contextBridge.exposeInMainWorld("moonlightNode", moonlightNode); 53 69 54 - const extCors = moonlightNode.processedExtensions.extensions 55 - .map((x) => x.manifest.cors ?? []) 56 - .flat(); 70 + const extCors = moonlightNode.processedExtensions.extensions.flatMap( 71 + (x) => x.manifest.cors ?? [] 72 + ); 57 73 58 74 for (const repo of moonlightNode.config.repositories) { 59 75 const url = new URL(repo); ··· 62 78 } 63 79 64 80 ipcRenderer.invoke(constants.ipcSetCorsList, extCors); 81 + 82 + const extBlocked = moonlightNode.processedExtensions.extensions.flatMap( 83 + (e) => e.manifest.blocked ?? [] 84 + ); 85 + ipcRenderer.invoke(constants.ipcSetBlockedList, extBlocked); 65 86 } 66 87 67 88 async function loadPreload() {
+1 -1
packages/types/package.json
··· 10 10 }, 11 11 "dependencies": { 12 12 "@moonlight-mod/lunast": "^1.0.0", 13 - "@moonlight-mod/mappings": "^1.0.0", 13 + "@moonlight-mod/mappings": "^1.0.2", 14 14 "@moonlight-mod/moonmap": "^1.0.2", 15 15 "@types/react": "^18.3.10", 16 16 "csstype": "^3.1.2",
+5
packages/types/src/constants.ts
··· 2 2 export const distDir = "dist"; 3 3 export const coreExtensionsDir = "core-extensions"; 4 4 export const repoUrlFile = ".moonlight-repo-url"; 5 + export const installedVersionFile = ".moonlight-installed-version"; 5 6 6 7 export const ipcGetOldPreloadPath = "_moonlight_getOldPreloadPath"; 7 8 export const ipcGetAppData = "_moonlight_getAppData"; 8 9 export const ipcGetIsMoonlightDesktop = "_moonlight_getIsMoonlightDesktop"; 9 10 export const ipcMessageBox = "_moonlight_messageBox"; 10 11 export const ipcSetCorsList = "_moonlight_setCorsList"; 12 + export const ipcSetBlockedList = "_moonlight_setBlockedList"; 11 13 12 14 export const apiLevel = 2; 15 + 16 + export const mainRepo = 17 + "https://moonlight-mod.github.io/extensions-dist/repo.json";
+2
packages/types/src/coreExtensions.ts
··· 2 2 export * as Settings from "./coreExtensions/settings"; 3 3 export * as Markdown from "./coreExtensions/markdown"; 4 4 export * as ContextMenu from "./coreExtensions/contextMenu"; 5 + export * as Notices from "./coreExtensions/notices"; 6 + export * as Moonbase from "./coreExtensions/moonbase";
+12
packages/types/src/coreExtensions/moonbase.ts
··· 1 + export type CustomComponent = React.FC<{ 2 + value: any; 3 + setValue: (value: any) => void; 4 + }>; 5 + 6 + export type Moonbase = { 7 + registerConfigComponent: ( 8 + ext: string, 9 + option: string, 10 + component: CustomComponent 11 + ) => void; 12 + };
+21
packages/types/src/coreExtensions/notices.ts
··· 1 + import type { Store } from "@moonlight-mod/mappings/discord/packages/flux"; 2 + 3 + export type NoticeButton = { 4 + name: string; 5 + onClick: () => boolean; // return true to dismiss the notice after the button is clicked 6 + }; 7 + 8 + export type Notice = { 9 + element: React.ReactNode; 10 + color?: string; 11 + showClose?: boolean; 12 + buttons?: NoticeButton[]; 13 + onDismiss?: () => void; 14 + }; 15 + 16 + export type Notices = Store<any> & { 17 + addNotice: (notice: Notice) => void; 18 + popNotice: () => void; 19 + getCurrentNotice: () => Notice | null; 20 + shouldShowNotice: () => boolean; 21 + };
+6
packages/types/src/discord/require.ts
··· 2 2 import { Markdown } from "../coreExtensions/markdown"; 3 3 import { Settings } from "../coreExtensions/settings"; 4 4 import { Spacepack } from "../coreExtensions/spacepack"; 5 + import { Notices } from "../coreExtensions/notices"; 6 + import { Moonbase } from "../coreExtensions/moonbase"; 5 7 6 8 declare function WebpackRequire(id: string): any; 7 9 ··· 9 11 declare function WebpackRequire(id: "contextMenu_contextMenu"): ContextMenu; 10 12 11 13 declare function WebpackRequire(id: "markdown_markdown"): Markdown; 14 + 15 + declare function WebpackRequire(id: "moonbase_moonbase"): Moonbase; 16 + 17 + declare function WebpackRequire(id: "notices_notices"): Notices; 12 18 13 19 declare function WebpackRequire(id: "settings_settings"): { 14 20 Settings: Settings;
+7 -5
packages/types/src/discord/webpack.ts
··· 1 1 import WebpackRequire from "./require"; 2 + import { WebpackRequire as MappingsWebpackRequire } from "@moonlight-mod/mappings"; 2 3 3 - export type WebpackRequireType = typeof WebpackRequire & { 4 - c: Record<string, WebpackModule>; 5 - m: Record<string, WebpackModuleFunc>; 6 - e: (module: number | string) => Promise<void>; 7 - }; 4 + export type WebpackRequireType = typeof MappingsWebpackRequire & 5 + typeof WebpackRequire & { 6 + c: Record<string, WebpackModule>; 7 + m: Record<string, WebpackModuleFunc>; 8 + e: (module: number | string) => Promise<void>; 9 + }; 8 10 9 11 export type WebpackModule = { 10 12 id: string | number;
+9
packages/types/src/extension.ts
··· 31 31 id: string; 32 32 version?: string; 33 33 apiLevel?: number; 34 + environment?: ExtensionEnvironment; 34 35 35 36 meta?: { 36 37 name?: string; ··· 47 48 incompatible?: string[]; 48 49 49 50 settings?: Record<string, ExtensionSettingsManifest>; 51 + 50 52 cors?: string[]; 53 + blocked?: string[]; 51 54 }; 55 + 56 + export enum ExtensionEnvironment { 57 + Both = "both", 58 + Desktop = "desktop", 59 + Web = "web" 60 + } 52 61 53 62 export enum ExtensionLoadSource { 54 63 Developer,
+17
packages/types/src/fs.ts
··· 1 + export type MoonlightFS = { 2 + readFile: (path: string) => Promise<Uint8Array>; 3 + readFileString: (path: string) => Promise<string>; 4 + writeFile: (path: string, data: Uint8Array) => Promise<void>; 5 + writeFileString: (path: string, data: string) => Promise<void>; 6 + unlink: (path: string) => Promise<void>; 7 + 8 + readdir: (path: string) => Promise<string[]>; 9 + mkdir: (path: string) => Promise<void>; 10 + rmdir: (path: string) => Promise<void>; 11 + 12 + exists: (path: string) => Promise<boolean>; 13 + isFile: (path: string) => Promise<boolean>; 14 + 15 + join: (...parts: string[]) => string; 16 + dirname: (path: string) => string; 17 + };
+23 -2
packages/types/src/globals.ts
··· 9 9 import type EventEmitter from "events"; 10 10 import type LunAST from "@moonlight-mod/lunast"; 11 11 import type Moonmap from "@moonlight-mod/moonmap"; 12 - import { EventPayloads, EventType, MoonlightEventEmitter } from "./core/event"; 12 + import type { 13 + EventPayloads, 14 + EventType, 15 + MoonlightEventEmitter 16 + } from "./core/event"; 13 17 14 18 export type MoonlightHost = { 15 19 asarPath: string; ··· 18 22 extensions: DetectedExtension[]; 19 23 processedExtensions: ProcessedExtensions; 20 24 25 + version: string; 26 + branch: MoonlightBranch; 27 + 21 28 getConfig: (ext: string) => ConfigExtension["config"]; 22 29 getConfigOption: <T>(ext: string, name: string) => T | undefined; 23 30 getLogger: (id: string) => Logger; ··· 28 35 extensions: DetectedExtension[]; 29 36 processedExtensions: ProcessedExtensions; 30 37 nativesCache: Record<string, any>; 38 + isBrowser: boolean; 39 + 40 + version: string; 41 + branch: MoonlightBranch; 31 42 32 43 getConfig: (ext: string) => ConfigExtension["config"]; 33 44 getConfigOption: <T>(ext: string, name: string) => T | undefined; 34 45 getNatives: (ext: string) => any | undefined; 35 46 getLogger: (id: string) => Logger; 36 47 48 + getMoonlightDir: () => string; 37 49 getExtensionDir: (ext: string) => string; 38 - writeConfig: (config: Config) => void; 50 + writeConfig: (config: Config) => Promise<void>; 39 51 }; 40 52 41 53 export type MoonlightWeb = { ··· 52 64 registerPatch: (patch: IdentifiedPatch) => void; 53 65 registerWebpackModule: (module: IdentifiedWebpackModule) => void; 54 66 }; 67 + 68 + version: string; 69 + branch: MoonlightBranch; 55 70 56 71 getConfig: (ext: string) => ConfigExtension["config"]; 57 72 getConfigOption: <T>(ext: string, name: string) => T | undefined; ··· 66 81 NodePreload = "node-preload", 67 82 WebPreload = "web-preload" 68 83 } 84 + 85 + export enum MoonlightBranch { 86 + STABLE = "stable", 87 + NIGHTLY = "nightly", 88 + DEV = "dev" 89 + }
+12
packages/types/src/import.d.ts
··· 17 17 export = Markdown; 18 18 } 19 19 20 + declare module "@moonlight-mod/wp/moonbase_moonbase" { 21 + import { CoreExtensions } from "@moonlight-mod/types"; 22 + const Moonbase: CoreExtensions.Moonbase.Moonbase; 23 + export = Moonbase; 24 + } 25 + 26 + declare module "@moonlight-mod/wp/notices_notices" { 27 + import { CoreExtensions } from "@moonlight-mod/types"; 28 + const Notices: CoreExtensions.Notices.Notices; 29 + export = Notices; 30 + } 31 + 20 32 declare module "@moonlight-mod/wp/settings_settings" { 21 33 import { CoreExtensions } from "@moonlight-mod/types"; 22 34 export const Settings: CoreExtensions.Settings.Settings;
+9
packages/types/src/index.ts
··· 4 4 /// <reference types="./mappings" /> 5 5 /* eslint-disable no-var */ 6 6 7 + import { MoonlightFS } from "./fs"; 7 8 import { 8 9 MoonlightEnv, 9 10 MoonlightHost, ··· 18 19 export * from "./globals"; 19 20 export * from "./logger"; 20 21 export * as constants from "./constants"; 22 + export * from "./fs"; 21 23 22 24 export type { AST } from "@moonlight-mod/lunast"; 23 25 export { ModuleExport, ModuleExportType } from "@moonlight-mod/moonmap"; ··· 28 30 const MOONLIGHT_INJECTOR: boolean; 29 31 const MOONLIGHT_NODE_PRELOAD: boolean; 30 32 const MOONLIGHT_WEB_PRELOAD: boolean; 33 + const MOONLIGHT_BROWSER: boolean; 34 + const MOONLIGHT_BRANCH: string; 35 + const MOONLIGHT_VERSION: string; 31 36 32 37 var moonlightHost: MoonlightHost; 33 38 var moonlightNode: MoonlightNode; 34 39 var moonlight: MoonlightWeb; 40 + var moonlightFS: MoonlightFS; 41 + 42 + var _moonlightBrowserInit: () => Promise<void>; 43 + var _moonlightBrowserLoad: () => Promise<void>; 35 44 }
+2 -1
packages/web-preload/package.json
··· 1 1 { 2 2 "name": "@moonlight-mod/web-preload", 3 3 "private": true, 4 + "main": "src/index.ts", 4 5 "dependencies": { 5 6 "@moonlight-mod/core": "workspace:*", 6 7 "@moonlight-mod/lunast": "^1.0.0", 7 - "@moonlight-mod/mappings": "^1.0.0", 8 + "@moonlight-mod/mappings": "^1.0.2", 8 9 "@moonlight-mod/moonmap": "^1.0.2", 9 10 "@moonlight-mod/types": "workspace:*" 10 11 }
+20 -6
packages/web-preload/src/index.ts
··· 5 5 registerPatch, 6 6 registerWebpackModule 7 7 } from "@moonlight-mod/core/patch"; 8 - import { constants } from "@moonlight-mod/types"; 8 + import { constants, MoonlightBranch } from "@moonlight-mod/types"; 9 9 import { installStyles } from "@moonlight-mod/core/styles"; 10 - import Logger from "@moonlight-mod/core/util/logger"; 10 + import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; 11 11 import LunAST from "@moonlight-mod/lunast"; 12 12 import Moonmap from "@moonlight-mod/moonmap"; 13 13 import loadMappings from "@moonlight-mod/mappings"; 14 14 import { createEventEmitter } from "@moonlight-mod/core/util/event"; 15 15 import { EventPayloads, EventType } from "@moonlight-mod/types/core/event"; 16 16 17 - (async () => { 17 + async function load() { 18 + initLogger(moonlightNode.config); 18 19 const logger = new Logger("web-preload"); 19 20 20 21 window.moonlight = { ··· 29 30 registerWebpackModule 30 31 }, 31 32 33 + version: MOONLIGHT_VERSION, 34 + branch: MOONLIGHT_BRANCH as MoonlightBranch, 35 + 32 36 getConfig: moonlightNode.getConfig.bind(moonlightNode), 33 37 getConfigOption: moonlightNode.getConfigOption.bind(moonlightNode), 34 38 getNatives: moonlightNode.getNatives.bind(moonlightNode), ··· 47 51 logger.error("Error setting up web-preload", e); 48 52 } 49 53 50 - window.addEventListener("DOMContentLoaded", () => { 54 + if (MOONLIGHT_ENV === "web-preload") { 55 + window.addEventListener("DOMContentLoaded", () => { 56 + installStyles(); 57 + }); 58 + } else { 51 59 installStyles(); 52 - }); 53 - })(); 60 + } 61 + } 62 + 63 + if (MOONLIGHT_ENV === "web-preload") { 64 + load(); 65 + } else { 66 + window._moonlightBrowserLoad = load; 67 + }
+182 -27
pnpm-lock.yaml
··· 42 42 specifier: ^5.3.2 43 43 version: 5.3.2 44 44 45 + packages/browser: 46 + dependencies: 47 + '@moonlight-mod/core': 48 + specifier: workspace:* 49 + version: link:../core 50 + '@moonlight-mod/types': 51 + specifier: workspace:* 52 + version: link:../types 53 + '@moonlight-mod/web-preload': 54 + specifier: workspace:* 55 + version: link:../web-preload 56 + '@zenfs/core': 57 + specifier: ^1.0.2 58 + version: 1.0.2 59 + '@zenfs/dom': 60 + specifier: ^0.2.16 61 + version: 0.2.16(@zenfs/core@1.0.2) 62 + 45 63 packages/core: 46 64 dependencies: 47 65 '@moonlight-mod/types': ··· 50 68 51 69 packages/core-extensions: 52 70 dependencies: 53 - '@electron/asar': 54 - specifier: ^3.2.5 55 - version: 3.2.5 71 + '@moonlight-mod/core': 72 + specifier: workspace:* 73 + version: link:../core 56 74 '@moonlight-mod/types': 57 75 specifier: workspace:* 58 76 version: link:../types 77 + nanotar: 78 + specifier: ^0.1.1 79 + version: 0.1.1 59 80 60 81 packages/injector: 61 82 dependencies: ··· 81 102 specifier: ^1.0.0 82 103 version: 1.0.0 83 104 '@moonlight-mod/mappings': 84 - specifier: ^1.0.0 85 - version: 1.0.0(@moonlight-mod/lunast@1.0.0)(@moonlight-mod/moonmap@1.0.2) 105 + specifier: ^1.0.2 106 + version: 1.0.2(@moonlight-mod/lunast@1.0.0)(@moonlight-mod/moonmap@1.0.2) 86 107 '@moonlight-mod/moonmap': 87 108 specifier: ^1.0.2 88 109 version: 1.0.2 ··· 105 126 specifier: ^1.0.0 106 127 version: 1.0.0 107 128 '@moonlight-mod/mappings': 108 - specifier: ^1.0.0 109 - version: 1.0.0(@moonlight-mod/lunast@1.0.0)(@moonlight-mod/moonmap@1.0.2) 129 + specifier: ^1.0.2 130 + version: 1.0.2(@moonlight-mod/lunast@1.0.0)(@moonlight-mod/moonmap@1.0.2) 110 131 '@moonlight-mod/moonmap': 111 132 specifier: ^1.0.2 112 133 version: 1.0.2 ··· 119 140 '@aashutoshrathi/word-wrap@1.2.6': 120 141 resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} 121 142 engines: {node: '>=0.10.0'} 122 - 123 - '@electron/asar@3.2.5': 124 - resolution: {integrity: sha512-Ypahc2ElTj9YOrFvUHuoXv5Z/V1nPA5enlhmQapc578m/HZBHKTbqhoL5JZQjje2+/6Ti5AHh7Gj1/haeJa63Q==} 125 - engines: {node: '>=10.12.0'} 126 - hasBin: true 127 143 128 144 '@esbuild/android-arm64@0.19.3': 129 145 resolution: {integrity: sha512-w+Akc0vv5leog550kjJV9Ru+MXMR2VuMrui3C61mnysim0gkFCPOUTAfzTP0qX+HpN9Syu3YA3p1hf3EPqObRw==} ··· 291 307 '@moonlight-mod/lunast@1.0.0': 292 308 resolution: {integrity: sha512-kJgf41K12i6/2LbXK97CNO+pNO7ADGh9N4bCQcOPwosocKMcwKHDEZUgPqeihNshY3c3AEW1LiyXjlsl24PdDw==} 293 309 294 - '@moonlight-mod/mappings@1.0.0': 295 - resolution: {integrity: sha512-n6ybbTqFxXVKvTHcIGEtCS9208an6BFnTojK56giWE/bjuqq6DvFrk/0+C3ZRDEjgohGf/DuUKc9f0qe9UH6Rg==} 310 + '@moonlight-mod/mappings@1.0.2': 311 + resolution: {integrity: sha512-PjIv4LFyt3j4LyGiokUmJ6a0L5JljoLXjUkixCynLLpNLd660qTcLe8f9tbhOovvD8joqejq+f5oqSo2V4/Vfg==} 296 312 peerDependencies: 297 313 '@moonlight-mod/lunast': ^1.0.0 298 314 '@moonlight-mod/moonmap': ^1.0.0 ··· 334 350 '@types/node@18.17.17': 335 351 resolution: {integrity: sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==} 336 352 353 + '@types/node@20.16.10': 354 + resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==} 355 + 337 356 '@types/prop-types@15.7.13': 338 357 resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} 339 358 340 359 '@types/react@18.3.10': 341 360 resolution: {integrity: sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==} 361 + 362 + '@types/readable-stream@4.0.15': 363 + resolution: {integrity: sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==} 342 364 343 365 '@types/semver@7.5.6': 344 366 resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} ··· 404 426 '@ungap/structured-clone@1.2.0': 405 427 resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} 406 428 429 + '@zenfs/core@1.0.2': 430 + resolution: {integrity: sha512-LMTD4ntn6Ag1y+IeOSVykDDvYC12dsGFtsX8M/54OQrLs7v+YnX4bpo0o2osbm8XFmU2MTNMX/G3PLsvzgWzrg==} 431 + engines: {node: '>= 16'} 432 + hasBin: true 433 + 434 + '@zenfs/dom@0.2.16': 435 + resolution: {integrity: sha512-6Ev+ol9hZIgQECNZR+xxjQ/a99EhhrWeiQttm/+U7YJK3HdTjiKfU39DsfGeH64vSqhpa5Vj+LWRx75SHkjw0Q==} 436 + engines: {node: '>= 18'} 437 + peerDependencies: 438 + '@zenfs/core': ^1.0.0 439 + 440 + abort-controller@3.0.0: 441 + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 442 + engines: {node: '>=6.5'} 443 + 407 444 acorn-jsx@5.3.2: 408 445 resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 409 446 peerDependencies: ··· 468 505 balanced-match@1.0.2: 469 506 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 470 507 508 + base64-js@1.5.1: 509 + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 510 + 471 511 big-integer@1.6.52: 472 512 resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} 473 513 engines: {node: '>=0.6'} ··· 479 519 brace-expansion@1.1.11: 480 520 resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 481 521 522 + brace-expansion@2.0.1: 523 + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 524 + 482 525 braces@3.0.2: 483 526 resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 484 527 engines: {node: '>=8'} 485 528 529 + buffer@6.0.3: 530 + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} 531 + 486 532 bundle-name@3.0.0: 487 533 resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} 488 534 engines: {node: '>=12'} ··· 504 550 505 551 color-name@1.1.4: 506 552 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 507 - 508 - commander@5.1.0: 509 - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} 510 - engines: {node: '>= 6'} 511 553 512 554 concat-map@0.0.1: 513 555 resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} ··· 633 675 eslint@8.55.0: 634 676 resolution: {integrity: sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==} 635 677 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 678 + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. 636 679 hasBin: true 637 680 638 681 espree@9.6.1: ··· 658 701 resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 659 702 engines: {node: '>=0.10.0'} 660 703 704 + event-target-shim@5.0.1: 705 + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} 706 + engines: {node: '>=6'} 707 + 708 + eventemitter3@5.0.1: 709 + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} 710 + 711 + events@3.3.0: 712 + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 713 + engines: {node: '>=0.8.x'} 714 + 661 715 execa@5.1.1: 662 716 resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} 663 717 engines: {node: '>=10'} ··· 800 854 engines: {node: '>=14'} 801 855 hasBin: true 802 856 857 + ieee754@1.2.1: 858 + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 859 + 803 860 ignore@5.3.0: 804 861 resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} 805 862 engines: {node: '>= 4'} ··· 1017 1074 minimatch@3.1.2: 1018 1075 resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1019 1076 1077 + minimatch@9.0.5: 1078 + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 1079 + engines: {node: '>=16 || 14 >=14.17'} 1080 + 1020 1081 ms@2.1.2: 1021 1082 resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 1083 + 1084 + nanotar@0.1.1: 1085 + resolution: {integrity: sha512-AiJsGsSF3O0havL1BydvI4+wR76sKT+okKRwWIaK96cZUnXqH0uNBOsHlbwZq3+m2BR1VKqHDVudl3gO4mYjpQ==} 1022 1086 1023 1087 natural-compare@1.4.0: 1024 1088 resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} ··· 1135 1199 engines: {node: '>=14'} 1136 1200 hasBin: true 1137 1201 1202 + process@0.11.10: 1203 + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} 1204 + engines: {node: '>= 0.6.0'} 1205 + 1138 1206 prop-types@15.8.1: 1139 1207 resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 1140 1208 ··· 1148 1216 react-is@16.13.1: 1149 1217 resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 1150 1218 1219 + readable-stream@4.5.2: 1220 + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} 1221 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 1222 + 1151 1223 reflect.getprototypeof@1.0.4: 1152 1224 resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} 1153 1225 engines: {node: '>= 0.4'} ··· 1184 1256 resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} 1185 1257 engines: {node: '>=0.4'} 1186 1258 1259 + safe-buffer@5.1.2: 1260 + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 1261 + 1262 + safe-buffer@5.2.1: 1263 + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1264 + 1187 1265 safe-regex-test@1.0.0: 1188 1266 resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} 1189 1267 ··· 1237 1315 1238 1316 string.prototype.trimstart@1.0.7: 1239 1317 resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} 1318 + 1319 + string_decoder@1.3.0: 1320 + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 1240 1321 1241 1322 strip-ansi@6.0.1: 1242 1323 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} ··· 1317 1398 unbox-primitive@1.0.2: 1318 1399 resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} 1319 1400 1401 + undici-types@6.19.8: 1402 + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 1403 + 1320 1404 untildify@4.0.0: 1321 1405 resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} 1322 1406 engines: {node: '>=8'} 1323 1407 1324 1408 uri-js@4.4.1: 1325 1409 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1410 + 1411 + utilium@0.7.1: 1412 + resolution: {integrity: sha512-2ocvTkI7U8LERmwxL0LhFUvEfN66UqcjF6tMiURvUwSyU7U1QC9gST+3iSUSiGccFfnP3f2EXwHNXOnOzx+lAg==} 1326 1413 1327 1414 which-boxed-primitive@1.0.2: 1328 1415 resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} ··· 1356 1443 snapshots: 1357 1444 1358 1445 '@aashutoshrathi/word-wrap@1.2.6': {} 1359 - 1360 - '@electron/asar@3.2.5': 1361 - dependencies: 1362 - commander: 5.1.0 1363 - glob: 7.2.3 1364 - minimatch: 3.1.2 1365 1446 1366 1447 '@esbuild/android-arm64@0.19.3': 1367 1448 optional: true ··· 1470 1551 estree-toolkit: 1.7.8 1471 1552 meriyah: 6.0.1 1472 1553 1473 - '@moonlight-mod/mappings@1.0.0(@moonlight-mod/lunast@1.0.0)(@moonlight-mod/moonmap@1.0.2)': 1554 + '@moonlight-mod/mappings@1.0.2(@moonlight-mod/lunast@1.0.0)(@moonlight-mod/moonmap@1.0.2)': 1474 1555 dependencies: 1475 1556 '@moonlight-mod/lunast': 1.0.0 1476 1557 '@moonlight-mod/moonmap': 1.0.2 ··· 1518 1599 1519 1600 '@types/node@18.17.17': {} 1520 1601 1602 + '@types/node@20.16.10': 1603 + dependencies: 1604 + undici-types: 6.19.8 1605 + 1521 1606 '@types/prop-types@15.7.13': {} 1522 1607 1523 1608 '@types/react@18.3.10': 1524 1609 dependencies: 1525 1610 '@types/prop-types': 15.7.13 1526 1611 csstype: 3.1.3 1612 + 1613 + '@types/readable-stream@4.0.15': 1614 + dependencies: 1615 + '@types/node': 20.16.10 1616 + safe-buffer: 5.1.2 1527 1617 1528 1618 '@types/semver@7.5.6': {} 1529 1619 ··· 1614 1704 1615 1705 '@ungap/structured-clone@1.2.0': {} 1616 1706 1707 + '@zenfs/core@1.0.2': 1708 + dependencies: 1709 + '@types/node': 20.16.10 1710 + '@types/readable-stream': 4.0.15 1711 + buffer: 6.0.3 1712 + eventemitter3: 5.0.1 1713 + minimatch: 9.0.5 1714 + readable-stream: 4.5.2 1715 + utilium: 0.7.1 1716 + 1717 + '@zenfs/dom@0.2.16(@zenfs/core@1.0.2)': 1718 + dependencies: 1719 + '@zenfs/core': 1.0.2 1720 + 1721 + abort-controller@3.0.0: 1722 + dependencies: 1723 + event-target-shim: 5.0.1 1724 + 1617 1725 acorn-jsx@5.3.2(acorn@8.12.1): 1618 1726 dependencies: 1619 1727 acorn: 8.12.1 ··· 1692 1800 1693 1801 balanced-match@1.0.2: {} 1694 1802 1803 + base64-js@1.5.1: {} 1804 + 1695 1805 big-integer@1.6.52: {} 1696 1806 1697 1807 bplist-parser@0.2.0: ··· 1702 1812 dependencies: 1703 1813 balanced-match: 1.0.2 1704 1814 concat-map: 0.0.1 1815 + 1816 + brace-expansion@2.0.1: 1817 + dependencies: 1818 + balanced-match: 1.0.2 1705 1819 1706 1820 braces@3.0.2: 1707 1821 dependencies: 1708 1822 fill-range: 7.0.1 1709 1823 1824 + buffer@6.0.3: 1825 + dependencies: 1826 + base64-js: 1.5.1 1827 + ieee754: 1.2.1 1828 + 1710 1829 bundle-name@3.0.0: 1711 1830 dependencies: 1712 1831 run-applescript: 5.0.0 ··· 1730 1849 1731 1850 color-name@1.1.4: {} 1732 1851 1733 - commander@5.1.0: {} 1734 - 1735 1852 concat-map@0.0.1: {} 1736 1853 1737 1854 cross-spawn@7.0.3: ··· 1997 2114 '@types/estree-jsx': 1.0.5 1998 2115 1999 2116 esutils@2.0.3: {} 2117 + 2118 + event-target-shim@5.0.1: {} 2119 + 2120 + eventemitter3@5.0.1: {} 2121 + 2122 + events@3.3.0: {} 2000 2123 2001 2124 execa@5.1.1: 2002 2125 dependencies: ··· 2160 2283 2161 2284 husky@8.0.3: {} 2162 2285 2286 + ieee754@1.2.1: {} 2287 + 2163 2288 ignore@5.3.0: {} 2164 2289 2165 2290 import-fresh@3.3.0: ··· 2359 2484 minimatch@3.1.2: 2360 2485 dependencies: 2361 2486 brace-expansion: 1.1.11 2487 + 2488 + minimatch@9.0.5: 2489 + dependencies: 2490 + brace-expansion: 2.0.1 2362 2491 2363 2492 ms@2.1.2: {} 2364 2493 2494 + nanotar@0.1.1: {} 2495 + 2365 2496 natural-compare@1.4.0: {} 2366 2497 2367 2498 npm-run-path@4.0.1: ··· 2472 2603 2473 2604 prettier@3.1.0: {} 2474 2605 2606 + process@0.11.10: {} 2607 + 2475 2608 prop-types@15.8.1: 2476 2609 dependencies: 2477 2610 loose-envify: 1.4.0 ··· 2483 2616 queue-microtask@1.2.3: {} 2484 2617 2485 2618 react-is@16.13.1: {} 2619 + 2620 + readable-stream@4.5.2: 2621 + dependencies: 2622 + abort-controller: 3.0.0 2623 + buffer: 6.0.3 2624 + events: 3.3.0 2625 + process: 0.11.10 2626 + string_decoder: 1.3.0 2486 2627 2487 2628 reflect.getprototypeof@1.0.4: 2488 2629 dependencies: ··· 2528 2669 has-symbols: 1.0.3 2529 2670 isarray: 2.0.5 2530 2671 2672 + safe-buffer@5.1.2: {} 2673 + 2674 + safe-buffer@5.2.1: {} 2675 + 2531 2676 safe-regex-test@1.0.0: 2532 2677 dependencies: 2533 2678 call-bind: 1.0.5 ··· 2602 2747 call-bind: 1.0.5 2603 2748 define-properties: 1.2.1 2604 2749 es-abstract: 1.22.3 2750 + 2751 + string_decoder@1.3.0: 2752 + dependencies: 2753 + safe-buffer: 5.2.1 2605 2754 2606 2755 strip-ansi@6.0.1: 2607 2756 dependencies: ··· 2680 2829 has-symbols: 1.0.3 2681 2830 which-boxed-primitive: 1.0.2 2682 2831 2832 + undici-types@6.19.8: {} 2833 + 2683 2834 untildify@4.0.0: {} 2684 2835 2685 2836 uri-js@4.4.1: 2686 2837 dependencies: 2687 2838 punycode: 2.3.1 2839 + 2840 + utilium@0.7.1: 2841 + dependencies: 2842 + eventemitter3: 5.0.1 2688 2843 2689 2844 which-boxed-primitive@1.0.2: 2690 2845 dependencies:
+70
scripts/link.js
··· 1 + // Janky script to get around pnpm link issues 2 + // Probably don't use this. Probably 3 + /* eslint-disable no-console */ 4 + const fs = require("fs"); 5 + const path = require("path"); 6 + const child_process = require("child_process"); 7 + 8 + const onDisk = { 9 + "@moonlight-mod/lunast": "../lunast", 10 + "@moonlight-mod/moonmap": "../moonmap", 11 + "@moonlight-mod/mappings": "../mappings" 12 + }; 13 + 14 + function exec(cmd, dir) { 15 + child_process.execSync(cmd, { cwd: dir, stdio: "inherit" }); 16 + } 17 + 18 + function getDeps(packageJSON) { 19 + const ret = {}; 20 + Object.assign(ret, packageJSON.dependencies || {}); 21 + Object.assign(ret, packageJSON.devDependencies || {}); 22 + Object.assign(ret, packageJSON.peerDependencies || {}); 23 + return ret; 24 + } 25 + 26 + function link(dir) { 27 + const packageJSON = JSON.parse( 28 + fs.readFileSync(path.join(dir, "package.json"), "utf8") 29 + ); 30 + const deps = getDeps(packageJSON); 31 + 32 + for (const [dep, path] of Object.entries(onDisk)) { 33 + if (deps[dep]) { 34 + exec(`pnpm link ${path}`, dir); 35 + } 36 + } 37 + } 38 + 39 + function undo(dir) { 40 + exec("pnpm unlink", dir); 41 + try { 42 + exec("git restore pnpm-lock.yaml", dir); 43 + } catch { 44 + // ignored 45 + } 46 + } 47 + 48 + const shouldUndo = process.argv.includes("--undo"); 49 + const packages = fs.readdirSync("./packages"); 50 + 51 + for (const path of Object.values(onDisk)) { 52 + console.log(path); 53 + if (shouldUndo) { 54 + undo(path); 55 + } else { 56 + link(path); 57 + } 58 + } 59 + 60 + if (shouldUndo) { 61 + const dir = __dirname; 62 + console.log(dir); 63 + undo(dir); 64 + } else { 65 + for (const pkg of packages) { 66 + const dir = path.join(__dirname, "packages", pkg); 67 + console.log(dir); 68 + link(dir); 69 + } 70 + }
+31
scripts/update.js
··· 1 + // Update dependencies in all packages 2 + /* eslint-disable no-console */ 3 + const fs = require("fs"); 4 + const path = require("path"); 5 + const child_process = require("child_process"); 6 + 7 + const packageToUpdate = process.argv[2]; 8 + 9 + function getDeps(packageJSON) { 10 + const ret = {}; 11 + Object.assign(ret, packageJSON.dependencies || {}); 12 + Object.assign(ret, packageJSON.devDependencies || {}); 13 + Object.assign(ret, packageJSON.peerDependencies || {}); 14 + return ret; 15 + } 16 + 17 + function exec(cmd, dir) { 18 + child_process.execSync(cmd, { cwd: dir, stdio: "inherit" }); 19 + } 20 + 21 + for (const package of fs.readdirSync("./packages")) { 22 + const packageJSON = JSON.parse( 23 + fs.readFileSync(path.join("./packages", package, "package.json"), "utf8") 24 + ); 25 + 26 + const deps = getDeps(packageJSON); 27 + if (Object.keys(deps).includes(packageToUpdate)) { 28 + console.log(`Updating ${packageToUpdate} in ${package}`); 29 + exec(`pnpm update ${packageToUpdate}`, path.join("./packages", package)); 30 + } 31 + }