Fix entry webpack modules not being patched
This commit is contained in:
		
							parent
							
								
									25f101602d
								
							
						
					
					
						commit
						e4701769a5
					
				
					 1 changed files with 189 additions and 163 deletions
				
			
		|  | @ -53,174 +53,35 @@ if (window[WEBPACK_CHUNK]) { | ||||||
|         }, |         }, | ||||||
|         configurable: true |         configurable: true | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     // wreq.m is the webpack module factory.
 | ||||||
|  |     // normally, this is populated via webpackGlobal.push, which we patch below.
 | ||||||
|  |     // However, Discord has their .m prepopulated.
 | ||||||
|  |     // Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
 | ||||||
|  |     Object.defineProperty(Function.prototype, "m", { | ||||||
|  |         set(v: any) { | ||||||
|  |             // When using react devtools or other extensions, we may also catch their webpack here.
 | ||||||
|  |             // This ensures we actually got the right one
 | ||||||
|  |             if (new Error().stack?.includes("discord.com")) { | ||||||
|  |                 logger.info("Found webpack module factory"); | ||||||
|  |                 patchFactories(v); | ||||||
|  | 
 | ||||||
|  |                 delete (Function.prototype as any).m; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             Object.defineProperty(this, "m", { | ||||||
|  |                 value: v, | ||||||
|  |                 configurable: true, | ||||||
|  |             }); | ||||||
|  |         }, | ||||||
|  |         configurable: true | ||||||
|  |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function patchPush(webpackGlobal: any) { | function patchPush(webpackGlobal: any) { | ||||||
|     function handlePush(chunk: any) { |     function handlePush(chunk: any) { | ||||||
|         try { |         try { | ||||||
|             const modules = chunk[1]; |             patchFactories(chunk[1]); | ||||||
|             const { subscriptions, listeners } = Vencord.Webpack; |  | ||||||
|             const { patches } = Vencord.Plugins; |  | ||||||
| 
 |  | ||||||
|             for (const id in modules) { |  | ||||||
|                 let mod = modules[id]; |  | ||||||
|                 // Discords Webpack chunks for some ungodly reason contain random
 |  | ||||||
|                 // newlines. Cyn recommended this workaround and it seems to work fine,
 |  | ||||||
|                 // however this could potentially break code, so if anything goes weird,
 |  | ||||||
|                 // this is probably why.
 |  | ||||||
|                 // Additionally, `[actual newline]` is one less char than "\n", so if Discord
 |  | ||||||
|                 // ever targets newer browsers, the minifier could potentially use this trick and
 |  | ||||||
|                 // cause issues.
 |  | ||||||
|                 let code: string = mod.toString().replaceAll("\n", ""); |  | ||||||
|                 // a very small minority of modules use function() instead of arrow functions,
 |  | ||||||
|                 // but, unnamed toplevel functions aren't valid. However 0, function() makes it a statement
 |  | ||||||
|                 if (code.startsWith("function(")) { |  | ||||||
|                     code = "0," + code; |  | ||||||
|                 } |  | ||||||
|                 const originalMod = mod; |  | ||||||
|                 const patchedBy = new Set(); |  | ||||||
| 
 |  | ||||||
|                 const factory = modules[id] = function (module, exports, require) { |  | ||||||
|                     try { |  | ||||||
|                         mod(module, exports, require); |  | ||||||
|                     } catch (err) { |  | ||||||
|                         // Just rethrow discord errors
 |  | ||||||
|                         if (mod === originalMod) throw err; |  | ||||||
| 
 |  | ||||||
|                         logger.error("Error in patched chunk", err); |  | ||||||
|                         return void originalMod(module, exports, require); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     exports = module.exports; |  | ||||||
| 
 |  | ||||||
|                     if (!exports) return; |  | ||||||
| 
 |  | ||||||
|                     // There are (at the time of writing) 11 modules exporting the window
 |  | ||||||
|                     // Make these non enumerable to improve webpack search performance
 |  | ||||||
|                     if (exports === window) { |  | ||||||
|                         Object.defineProperty(require.c, id, { |  | ||||||
|                             value: require.c[id], |  | ||||||
|                             enumerable: false, |  | ||||||
|                             configurable: true, |  | ||||||
|                             writable: true |  | ||||||
|                         }); |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     const numberId = Number(id); |  | ||||||
| 
 |  | ||||||
|                     for (const callback of listeners) { |  | ||||||
|                         try { |  | ||||||
|                             callback(exports, numberId); |  | ||||||
|                         } catch (err) { |  | ||||||
|                             logger.error("Error in webpack listener", err); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     for (const [filter, callback] of subscriptions) { |  | ||||||
|                         try { |  | ||||||
|                             if (filter(exports)) { |  | ||||||
|                                 subscriptions.delete(filter); |  | ||||||
|                                 callback(exports, numberId); |  | ||||||
|                             } else if (typeof exports === "object") { |  | ||||||
|                                 if (exports.default && filter(exports.default)) { |  | ||||||
|                                     subscriptions.delete(filter); |  | ||||||
|                                     callback(exports.default, numberId); |  | ||||||
|                                 } |  | ||||||
| 
 |  | ||||||
|                                 for (const nested in exports) if (nested.length <= 3) { |  | ||||||
|                                     if (exports[nested] && filter(exports[nested])) { |  | ||||||
|                                         subscriptions.delete(filter); |  | ||||||
|                                         callback(exports[nested], numberId); |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } catch (err) { |  | ||||||
|                             logger.error("Error while firing callback for webpack chunk", err); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } as any as { toString: () => string, original: any, (...args: any[]): void; }; |  | ||||||
| 
 |  | ||||||
|                 // for some reason throws some error on which calling .toString() leads to infinite recursion
 |  | ||||||
|                 // when you force load all chunks???
 |  | ||||||
|                 try { |  | ||||||
|                     factory.toString = () => mod.toString(); |  | ||||||
|                     factory.original = originalMod; |  | ||||||
|                 } catch { } |  | ||||||
| 
 |  | ||||||
|                 for (let i = 0; i < patches.length; i++) { |  | ||||||
|                     const patch = patches[i]; |  | ||||||
|                     const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); |  | ||||||
|                     if (patch.predicate && !patch.predicate()) continue; |  | ||||||
| 
 |  | ||||||
|                     if (code.includes(patch.find)) { |  | ||||||
|                         patchedBy.add(patch.plugin); |  | ||||||
| 
 |  | ||||||
|                         // we change all patch.replacement to array in plugins/index
 |  | ||||||
|                         for (const replacement of patch.replacement as PatchReplacement[]) { |  | ||||||
|                             if (replacement.predicate && !replacement.predicate()) continue; |  | ||||||
|                             const lastMod = mod; |  | ||||||
|                             const lastCode = code; |  | ||||||
| 
 |  | ||||||
|                             canonicalizeReplacement(replacement, patch.plugin); |  | ||||||
| 
 |  | ||||||
|                             try { |  | ||||||
|                                 const newCode = executePatch(replacement.match, replacement.replace as string); |  | ||||||
|                                 if (newCode === code && !patch.noWarn) { |  | ||||||
|                                     (window.explosivePlugins ??= new Set<string>()).add(patch.plugin); |  | ||||||
|                                     logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); |  | ||||||
|                                     if (IS_DEV) { |  | ||||||
|                                         logger.debug("Function Source:\n", code); |  | ||||||
|                                     } |  | ||||||
|                                 } else { |  | ||||||
|                                     code = newCode; |  | ||||||
|                                     mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); |  | ||||||
|                                 } |  | ||||||
|                             } catch (err) { |  | ||||||
|                                 logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); |  | ||||||
| 
 |  | ||||||
|                                 if (IS_DEV) { |  | ||||||
|                                     const changeSize = code.length - lastCode.length; |  | ||||||
|                                     const match = lastCode.match(replacement.match)!; |  | ||||||
| 
 |  | ||||||
|                                     // Use 200 surrounding characters of context
 |  | ||||||
|                                     const start = Math.max(0, match.index! - 200); |  | ||||||
|                                     const end = Math.min(lastCode.length, match.index! + match[0].length + 200); |  | ||||||
|                                     // (changeSize may be negative)
 |  | ||||||
|                                     const endPatched = end + changeSize; |  | ||||||
| 
 |  | ||||||
|                                     const context = lastCode.slice(start, end); |  | ||||||
|                                     const patchedContext = code.slice(start, endPatched); |  | ||||||
| 
 |  | ||||||
|                                     // inline require to avoid including it in !IS_DEV builds
 |  | ||||||
|                                     const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); |  | ||||||
|                                     let fmt = "%c %s "; |  | ||||||
|                                     const elements = [] as string[]; |  | ||||||
|                                     for (const d of diff) { |  | ||||||
|                                         const color = d.removed |  | ||||||
|                                             ? "red" |  | ||||||
|                                             : d.added |  | ||||||
|                                                 ? "lime" |  | ||||||
|                                                 : "grey"; |  | ||||||
|                                         fmt += "%c%s"; |  | ||||||
|                                         elements.push("color:" + color, d.value); |  | ||||||
|                                     } |  | ||||||
| 
 |  | ||||||
|                                     logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); |  | ||||||
|                                     logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); |  | ||||||
|                                     const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); |  | ||||||
|                                     logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); |  | ||||||
|                                 } |  | ||||||
|                                 code = lastCode; |  | ||||||
|                                 mod = lastMod; |  | ||||||
|                                 patchedBy.delete(patch.plugin); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         if (!patch.all) patches.splice(i--, 1); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } catch (err) { |         } catch (err) { | ||||||
|             logger.error("Error in handlePush", err); |             logger.error("Error in handlePush", err); | ||||||
|         } |         } | ||||||
|  | @ -244,3 +105,168 @@ function patchPush(webpackGlobal: any) { | ||||||
|         configurable: true |         configurable: true | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | function patchFactories(factories: Record<string | number, (module: { exports: any; }, exports: any, require: any) => void>) { | ||||||
|  |     const { subscriptions, listeners } = Vencord.Webpack; | ||||||
|  |     const { patches } = Vencord.Plugins; | ||||||
|  | 
 | ||||||
|  |     for (const id in factories) { | ||||||
|  |         let mod = factories[id]; | ||||||
|  |         // Discords Webpack chunks for some ungodly reason contain random
 | ||||||
|  |         // newlines. Cyn recommended this workaround and it seems to work fine,
 | ||||||
|  |         // however this could potentially break code, so if anything goes weird,
 | ||||||
|  |         // this is probably why.
 | ||||||
|  |         // Additionally, `[actual newline]` is one less char than "\n", so if Discord
 | ||||||
|  |         // ever targets newer browsers, the minifier could potentially use this trick and
 | ||||||
|  |         // cause issues.
 | ||||||
|  |         let code: string = mod.toString().replaceAll("\n", ""); | ||||||
|  |         // a very small minority of modules use function() instead of arrow functions,
 | ||||||
|  |         // but, unnamed toplevel functions aren't valid. However 0, function() makes it a statement
 | ||||||
|  |         if (code.startsWith("function(")) { | ||||||
|  |             code = "0," + code; | ||||||
|  |         } | ||||||
|  |         const originalMod = mod; | ||||||
|  |         const patchedBy = new Set(); | ||||||
|  | 
 | ||||||
|  |         const factory = factories[id] = function (module, exports, require) { | ||||||
|  |             try { | ||||||
|  |                 mod(module, exports, require); | ||||||
|  |             } catch (err) { | ||||||
|  |                 // Just rethrow discord errors
 | ||||||
|  |                 if (mod === originalMod) throw err; | ||||||
|  | 
 | ||||||
|  |                 logger.error("Error in patched chunk", err); | ||||||
|  |                 return void originalMod(module, exports, require); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             exports = module.exports; | ||||||
|  | 
 | ||||||
|  |             if (!exports) return; | ||||||
|  | 
 | ||||||
|  |             // There are (at the time of writing) 11 modules exporting the window
 | ||||||
|  |             // Make these non enumerable to improve webpack search performance
 | ||||||
|  |             if (exports === window) { | ||||||
|  |                 Object.defineProperty(require.c, id, { | ||||||
|  |                     value: require.c[id], | ||||||
|  |                     enumerable: false, | ||||||
|  |                     configurable: true, | ||||||
|  |                     writable: true | ||||||
|  |                 }); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const numberId = Number(id); | ||||||
|  | 
 | ||||||
|  |             for (const callback of listeners) { | ||||||
|  |                 try { | ||||||
|  |                     callback(exports, numberId); | ||||||
|  |                 } catch (err) { | ||||||
|  |                     logger.error("Error in webpack listener", err); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             for (const [filter, callback] of subscriptions) { | ||||||
|  |                 try { | ||||||
|  |                     if (filter(exports)) { | ||||||
|  |                         subscriptions.delete(filter); | ||||||
|  |                         callback(exports, numberId); | ||||||
|  |                     } else if (typeof exports === "object") { | ||||||
|  |                         if (exports.default && filter(exports.default)) { | ||||||
|  |                             subscriptions.delete(filter); | ||||||
|  |                             callback(exports.default, numberId); | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         for (const nested in exports) if (nested.length <= 3) { | ||||||
|  |                             if (exports[nested] && filter(exports[nested])) { | ||||||
|  |                                 subscriptions.delete(filter); | ||||||
|  |                                 callback(exports[nested], numberId); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } catch (err) { | ||||||
|  |                     logger.error("Error while firing callback for webpack chunk", err); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } as any as { toString: () => string, original: any, (...args: any[]): void; }; | ||||||
|  | 
 | ||||||
|  |         // for some reason throws some error on which calling .toString() leads to infinite recursion
 | ||||||
|  |         // when you force load all chunks???
 | ||||||
|  |         try { | ||||||
|  |             factory.toString = () => mod.toString(); | ||||||
|  |             factory.original = originalMod; | ||||||
|  |         } catch { } | ||||||
|  | 
 | ||||||
|  |         for (let i = 0; i < patches.length; i++) { | ||||||
|  |             const patch = patches[i]; | ||||||
|  |             const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); | ||||||
|  |             if (patch.predicate && !patch.predicate()) continue; | ||||||
|  | 
 | ||||||
|  |             if (code.includes(patch.find)) { | ||||||
|  |                 patchedBy.add(patch.plugin); | ||||||
|  | 
 | ||||||
|  |                 // we change all patch.replacement to array in plugins/index
 | ||||||
|  |                 for (const replacement of patch.replacement as PatchReplacement[]) { | ||||||
|  |                     if (replacement.predicate && !replacement.predicate()) continue; | ||||||
|  |                     const lastMod = mod; | ||||||
|  |                     const lastCode = code; | ||||||
|  | 
 | ||||||
|  |                     canonicalizeReplacement(replacement, patch.plugin); | ||||||
|  | 
 | ||||||
|  |                     try { | ||||||
|  |                         const newCode = executePatch(replacement.match, replacement.replace as string); | ||||||
|  |                         if (newCode === code && !patch.noWarn) { | ||||||
|  |                             (window.explosivePlugins ??= new Set<string>()).add(patch.plugin); | ||||||
|  |                             logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); | ||||||
|  |                             if (IS_DEV) { | ||||||
|  |                                 logger.debug("Function Source:\n", code); | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             code = newCode; | ||||||
|  |                             mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); | ||||||
|  |                         } | ||||||
|  |                     } catch (err) { | ||||||
|  |                         logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); | ||||||
|  | 
 | ||||||
|  |                         if (IS_DEV) { | ||||||
|  |                             const changeSize = code.length - lastCode.length; | ||||||
|  |                             const match = lastCode.match(replacement.match)!; | ||||||
|  | 
 | ||||||
|  |                             // Use 200 surrounding characters of context
 | ||||||
|  |                             const start = Math.max(0, match.index! - 200); | ||||||
|  |                             const end = Math.min(lastCode.length, match.index! + match[0].length + 200); | ||||||
|  |                             // (changeSize may be negative)
 | ||||||
|  |                             const endPatched = end + changeSize; | ||||||
|  | 
 | ||||||
|  |                             const context = lastCode.slice(start, end); | ||||||
|  |                             const patchedContext = code.slice(start, endPatched); | ||||||
|  | 
 | ||||||
|  |                             // inline require to avoid including it in !IS_DEV builds
 | ||||||
|  |                             const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); | ||||||
|  |                             let fmt = "%c %s "; | ||||||
|  |                             const elements = [] as string[]; | ||||||
|  |                             for (const d of diff) { | ||||||
|  |                                 const color = d.removed | ||||||
|  |                                     ? "red" | ||||||
|  |                                     : d.added | ||||||
|  |                                         ? "lime" | ||||||
|  |                                         : "grey"; | ||||||
|  |                                 fmt += "%c%s"; | ||||||
|  |                                 elements.push("color:" + color, d.value); | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|  |                             logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); | ||||||
|  |                             logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); | ||||||
|  |                             const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); | ||||||
|  |                             logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); | ||||||
|  |                         } | ||||||
|  |                         code = lastCode; | ||||||
|  |                         mod = lastMod; | ||||||
|  |                         patchedBy.delete(patch.plugin); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (!patch.all) patches.splice(i--, 1); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue