this repo has no description
at v1.0.2 10 kB view raw
1import { 2 PatchReplace, 3 PatchReplaceType, 4 ExplicitExtensionDependency, 5 IdentifiedPatch, 6 IdentifiedWebpackModule, 7 WebpackJsonp, 8 WebpackJsonpEntry, 9 WebpackModuleFunc 10} from "@moonlight-mod/types"; 11import Logger from "./util/logger"; 12import calculateDependencies, { Dependency } from "./util/dependency"; 13import WebpackRequire from "@moonlight-mod/types/discord/require"; 14 15const logger = new Logger("core/patch"); 16 17// Can't be Set because we need splice 18const patches: IdentifiedPatch[] = []; 19let webpackModules: Set<IdentifiedWebpackModule> = new Set(); 20 21export function registerPatch(patch: IdentifiedPatch) { 22 patches.push(patch); 23} 24 25export function registerWebpackModule(wp: IdentifiedWebpackModule) { 26 webpackModules.add(wp); 27} 28 29/* 30 The patching system functions by matching a string or regex against the 31 .toString()'d copy of a Webpack module. When a patch happens, we reconstruct 32 the module with the patched source and replace it, wrapping it in the process. 33 34 We keep track of what modules we've patched (and their original sources), both 35 so we don't wrap them twice and so we can debug what extensions are patching 36 what Webpack modules. 37*/ 38const moduleCache: Record<string, string> = {}; 39const patched: Record<string, Array<string>> = {}; 40 41function patchModules(entry: WebpackJsonpEntry[1]) { 42 for (const [id, func] of Object.entries(entry)) { 43 let moduleString = Object.prototype.hasOwnProperty.call(moduleCache, id) 44 ? moduleCache[id] 45 : func.toString().replace(/\n/g, ""); 46 47 for (const patch of patches) { 48 if (patch.prerequisite != null && !patch.prerequisite()) { 49 continue; 50 } 51 52 if (patch.find instanceof RegExp && patch.find.global) { 53 // Reset state because global regexes are stateful for some reason 54 patch.find.lastIndex = 0; 55 } 56 57 // indexOf is faster than includes by 0.25% lmao 58 const match = 59 typeof patch.find === "string" 60 ? moduleString.indexOf(patch.find) !== -1 61 : patch.find.test(moduleString); 62 63 // Global regexes apply to all modules 64 const shouldRemove = 65 typeof patch.find === "string" ? true : !patch.find.global; 66 67 if (match) { 68 moonlight.unpatched.delete(patch); 69 70 // We ensured all arrays get turned into normal PatchReplace objects on register 71 const replace = patch.replace as PatchReplace; 72 73 if ( 74 replace.type === undefined || 75 replace.type === PatchReplaceType.Normal 76 ) { 77 // tsc fails to detect the overloads for this, so I'll just do this 78 // Verbose, but it works 79 let replaced; 80 if (typeof replace.replacement === "string") { 81 replaced = moduleString.replace(replace.match, replace.replacement); 82 } else { 83 replaced = moduleString.replace(replace.match, replace.replacement); 84 } 85 86 if (replaced === moduleString) { 87 logger.warn("Patch replacement failed", id, patch); 88 continue; 89 } 90 91 // Store what extensions patched what modules for easier debugging 92 patched[id] = patched[id] || []; 93 patched[id].push(`${patch.ext}#${patch.id}`); 94 95 // Webpack module arguments are minified, so we replace them with consistent names 96 // We have to wrap it so things don't break, though 97 const patchedStr = patched[id].sort().join(", "); 98 99 const wrapped = 100 `(${replaced}).apply(this, arguments)\n` + 101 `// Patched by moonlight: ${patchedStr}\n` + 102 `//# sourceURL=Webpack-Module-${id}`; 103 104 try { 105 const func = new Function( 106 "module", 107 "exports", 108 "require", 109 wrapped 110 ) as WebpackModuleFunc; 111 entry[id] = func; 112 entry[id].__moonlight = true; 113 moduleString = replaced; 114 } catch (e) { 115 logger.warn("Error constructing function for patch", e); 116 } 117 } else if (replace.type === PatchReplaceType.Module) { 118 // Directly replace the module with a new one 119 const newModule = replace.replacement(moduleString); 120 entry[id] = newModule; 121 entry[id].__moonlight = true; 122 moduleString = 123 newModule.toString().replace(/\n/g, "") + 124 `//# sourceURL=Webpack-Module-${id}`; 125 } 126 127 if (shouldRemove) { 128 patches.splice( 129 patches.findIndex((p) => p.ext === patch.ext && p.id === patch.id), 130 1 131 ); 132 } 133 } 134 } 135 136 if (moonlightNode.config.patchAll === true) { 137 if ( 138 (typeof id !== "string" || !id.includes("_")) && 139 !entry[id].__moonlight 140 ) { 141 const wrapped = 142 `(${moduleString}).apply(this, arguments)\n` + 143 `//# sourceURL=Webpack-Module-${id}`; 144 entry[id] = new Function( 145 "module", 146 "exports", 147 "require", 148 wrapped 149 ) as WebpackModuleFunc; 150 entry[id].__moonlight = true; 151 } 152 } 153 154 moduleCache[id] = moduleString; 155 } 156} 157 158/* 159 Similar to patching, we also want to inject our own custom Webpack modules 160 into Discord's Webpack instance. We abuse pollution on the push function to 161 mark when we've completed it already. 162*/ 163let chunkId = Number.MAX_SAFE_INTEGER; 164 165function handleModuleDependencies() { 166 const modules = Array.from(webpackModules.values()); 167 168 const dependencies: Dependency<string, IdentifiedWebpackModule>[] = 169 modules.map((wp) => { 170 return { 171 id: `${wp.ext}_${wp.id}`, 172 data: wp 173 }; 174 }); 175 176 const [sorted, _] = calculateDependencies( 177 dependencies, 178 179 function fetchDep(id) { 180 return modules.find((x) => id === `${x.ext}_${x.id}`) ?? null; 181 }, 182 183 function getDeps(item) { 184 const deps = item.data?.dependencies ?? []; 185 return ( 186 deps.filter( 187 (dep) => !(dep instanceof RegExp || typeof dep === "string") 188 ) as ExplicitExtensionDependency[] 189 ).map((x) => `${x.ext}_${x.id}`); 190 } 191 ); 192 193 webpackModules = new Set(sorted.map((x) => x.data)); 194} 195 196const injectedWpModules: IdentifiedWebpackModule[] = []; 197function injectModules(entry: WebpackJsonpEntry[1]) { 198 const modules: Record<string, WebpackModuleFunc> = {}; 199 const entrypoints: string[] = []; 200 let inject = false; 201 202 for (const [_modId, mod] of Object.entries(entry)) { 203 const modStr = mod.toString(); 204 const wpModules = Array.from(webpackModules.values()); 205 for (const wpModule of wpModules) { 206 const id = wpModule.ext + "_" + wpModule.id; 207 if (wpModule.dependencies) { 208 const deps = new Set(wpModule.dependencies); 209 210 // FIXME: This dependency resolution might fail if the things we want 211 // got injected earlier. If weird dependencies fail, this is likely why. 212 if (deps.size) { 213 for (const dep of deps.values()) { 214 if (typeof dep === "string") { 215 if (modStr.includes(dep)) deps.delete(dep); 216 } else if (dep instanceof RegExp) { 217 if (dep.test(modStr)) deps.delete(dep); 218 } else if ( 219 injectedWpModules.find( 220 (x) => x.ext === dep.ext && x.id === dep.id 221 ) 222 ) { 223 deps.delete(dep); 224 } 225 } 226 227 if (deps.size !== 0) { 228 // Update the deps that have passed 229 webpackModules.delete(wpModule); 230 wpModule.dependencies = Array.from(deps); 231 webpackModules.add(wpModule); 232 continue; 233 } 234 235 wpModule.dependencies = Array.from(deps); 236 } 237 } 238 239 webpackModules.delete(wpModule); 240 injectedWpModules.push(wpModule); 241 242 inject = true; 243 244 modules[id] = wpModule.run; 245 if (wpModule.entrypoint) entrypoints.push(id); 246 } 247 if (!webpackModules.size) break; 248 } 249 250 if (inject) { 251 logger.debug("Injecting modules:", modules, entrypoints); 252 window.webpackChunkdiscord_app.push([ 253 [--chunkId], 254 modules, 255 (require: typeof WebpackRequire) => entrypoints.map(require) 256 ]); 257 } 258} 259 260declare global { 261 interface Window { 262 webpackChunkdiscord_app: WebpackJsonp; 263 } 264} 265 266/* 267 Webpack modules are bundled into an array of arrays that hold each function. 268 Since we run code before Discord, we can create our own Webpack array and 269 hijack the .push function on it. 270 271 From there, we iterate over the object (mapping IDs to functions) and patch 272 them accordingly. 273*/ 274export async function installWebpackPatcher() { 275 await handleModuleDependencies(); 276 277 let realWebpackJsonp: WebpackJsonp | null = null; 278 Object.defineProperty(window, "webpackChunkdiscord_app", { 279 set: (jsonp: WebpackJsonp) => { 280 // Don't let Sentry mess with Webpack 281 const stack = new Error().stack!; 282 if (stack.includes("sentry.")) return; 283 284 realWebpackJsonp = jsonp; 285 const realPush = jsonp.push; 286 if (jsonp.push.__moonlight !== true) { 287 jsonp.push = (items) => { 288 patchModules(items[1]); 289 290 try { 291 const res = realPush.apply(realWebpackJsonp, [items]); 292 if (!realPush.__moonlight) { 293 logger.trace("Injecting Webpack modules", items[1]); 294 injectModules(items[1]); 295 } 296 297 return res; 298 } catch (err) { 299 logger.error("Failed to inject Webpack modules:", err); 300 return 0; 301 } 302 }; 303 304 jsonp.push.bind = (thisArg: any, ...args: any[]) => { 305 return realPush.bind(thisArg, ...args); 306 }; 307 308 jsonp.push.__moonlight = true; 309 if (!realPush.__moonlight) { 310 logger.debug("Injecting Webpack modules with empty entry"); 311 // Inject an empty entry to cause iteration to happen once 312 // Kind of a dirty hack but /shrug 313 injectModules({ deez: () => {} }); 314 } 315 } 316 }, 317 318 get: () => { 319 const stack = new Error().stack!; 320 if (stack.includes("sentry.")) return []; 321 return realWebpackJsonp; 322 } 323 }); 324 325 registerWebpackModule({ 326 ext: "moonlight", 327 id: "fix_rspack_init_modules", 328 entrypoint: true, 329 run: function (module, exports, require) { 330 patchModules(require.m); 331 } 332 }); 333}