@ -18,41 +18,94 @@
import { WEBPACK_CHUNK } from "@utils/constants" ;
import { WEBPACK_CHUNK } from "@utils/constants" ;
import { Logger } from "@utils/Logger" ;
import { Logger } from "@utils/Logger" ;
import { canonicalize Replacement } from "@utils/patches" ;
import { canonicalize Match, canonicalize Replacement } from "@utils/patches" ;
import { PatchReplacement } from "@utils/types" ;
import { PatchReplacement } from "@utils/types" ;
import { WebpackInstance } from "discord-types/other" ;
import { traceFunction } from "../debug/Tracer" ;
import { traceFunction } from "../debug/Tracer" ;
import { _initWebpack } from "." ;
import { patches } from "../plugins" ;
import { _initWebpack , beforeInitListeners , factoryListeners , module Listeners , subscriptions , wreq } from "." ;
let webpackChunk : any [ ] ;
const logger = new Logger ( "WebpackInterceptor" , "#8caaee" ) ;
const logger = new Logger ( "WebpackInterceptor" , "#8caaee" ) ;
const initCallbackRegex = canonicalizeMatch ( /{return \i\(".+?"\)}/ ) ;
let webpackChunk : any [ ] ;
if ( window [ WEBPACK_CHUNK ] ) {
// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed
logger . info ( ` Patching ${ WEBPACK_CHUNK } .push (was already existent, likely from cache!) ` ) ;
// This way we can patch the factory of everything being pushed to the modules array
_initWebpack ( window [ WEBPACK_CHUNK ] ) ;
patchPush ( window [ WEBPACK_CHUNK ] ) ;
} else {
Object . defineProperty ( window , WEBPACK_CHUNK , {
Object . defineProperty ( window , WEBPACK_CHUNK , {
configurable : true ,
get : ( ) = > webpackChunk ,
get : ( ) = > webpackChunk ,
set : v = > {
set : v = > {
if ( v ? . push ) {
if ( v ? . push ) {
if ( ! v . push . $ $vencordOriginal ) {
if ( ! v . push . $ $vencordOriginal ) {
logger . info ( ` Patching ${ WEBPACK_CHUNK } .push ` ) ;
logger . info ( ` Patching ${ WEBPACK_CHUNK } .push ` ) ;
patchPush ( v ) ;
patchPush ( v ) ;
}
if ( _initWebpack ( v ) ) {
logger . info ( "Successfully initialised Vencord webpack" ) ;
// @ts-ignore
// @ts-ignore
delete window [ WEBPACK_CHUNK ] ;
delete window [ WEBPACK_CHUNK ] ;
window [ WEBPACK_CHUNK ] = v ;
window [ WEBPACK_CHUNK ] = v ;
}
}
}
}
webpackChunk = v ;
webpackChunk = v ;
} ,
}
} ) ;
// wreq.O is the webpack onChunksLoaded function
// Discord uses it to await for all the chunks to be loaded before initializing the app
// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it
Object . defineProperty ( Function . prototype , "O" , {
configurable : true ,
set ( onChunksLoaded : any ) {
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
// This ensures we actually got the right one
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
if ( new Error ( ) . stack ? . includes ( "discord.com" ) && String ( this . e ) . includes ( "Promise.all" ) ) {
logger . info ( "Found main WebpackRequire.onChunksLoaded" ) ;
delete ( Function . prototype as any ) . O ;
const originalOnChunksLoaded = onChunksLoaded ;
onChunksLoaded = function ( this : unknown , result : any , chunkIds : string [ ] , callback : ( ) = > any , priority : number ) {
if ( callback != null && initCallbackRegex . test ( callback . toString ( ) ) ) {
Object . defineProperty ( this , "O" , {
value : originalOnChunksLoaded ,
configurable : true
} ) ;
const wreq = this as WebpackInstance ;
const originalCallback = callback ;
callback = function ( this : unknown ) {
logger . info ( "Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners" ) ;
_initWebpack ( wreq ) ;
for ( const beforeInitListener of beforeInitListeners ) {
beforeInitListener ( wreq ) ;
}
originalCallback . apply ( this , arguments as any ) ;
} ;
callback . toString = originalCallback . toString . bind ( originalCallback ) ;
arguments [ 2 ] = callback ;
}
originalOnChunksLoaded . apply ( this , arguments as any ) ;
} ;
onChunksLoaded . toString = originalOnChunksLoaded . toString . bind ( originalOnChunksLoaded ) ;
}
Object . defineProperty ( this , "O" , {
value : onChunksLoaded ,
configurable : true
configurable : true
} ) ;
} ) ;
}
} ) ;
// wreq.m is the webpack module factory.
// wreq.m is the webpack module factory.
// normally, this is populated via webpackGlobal.push, which we patch below.
// normally, this is populated via webpackGlobal.push, which we patch below.
@ -62,22 +115,23 @@ if (window[WEBPACK_CHUNK]) {
// Update: Discord now has TWO webpack instances. Their normal one and sentry
// Update: Discord now has TWO webpack instances. Their normal one and sentry
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
Object . defineProperty ( Function . prototype , "m" , {
Object . defineProperty ( Function . prototype , "m" , {
configurable : true ,
set ( v : any ) {
set ( v : any ) {
// When using react devtools or other extensions, we may also catch their webpack here.
// When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one
// This ensures we actually got the right one
if ( new Error ( ) . stack ? . includes ( "discord.com" ) ) {
const error = new Error ( ) ;
logger . info ( "Found webpack module factory" ) ;
if ( error . stack ? . includes ( "discord.com" ) ) {
logger . info ( "Found Webpack module factory" , error . stack . match ( /\/assets\/(.+?\.js)/ ) ? . [ 1 ] ? ? "" ) ;
patchFactories ( v ) ;
patchFactories ( v ) ;
}
}
Object . defineProperty ( this , "m" , {
Object . defineProperty ( this , "m" , {
value : v ,
value : v ,
configurable : true ,
} ) ;
} ,
configurable : true
configurable : true
} ) ;
} ) ;
}
}
} ) ;
function patchPush ( webpackGlobal : any ) {
function patchPush ( webpackGlobal : any ) {
function handlePush ( chunk : any ) {
function handlePush ( chunk : any ) {
@ -91,6 +145,7 @@ function patchPush(webpackGlobal: any) {
}
}
handlePush . $ $vencordOriginal = webpackGlobal . push ;
handlePush . $ $vencordOriginal = webpackGlobal . push ;
handlePush . toString = handlePush . $ $vencordOriginal . toString . bind ( handlePush . $ $vencordOriginal ) ;
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
@ -99,41 +154,41 @@ function patchPush(webpackGlobal: any) {
handlePush . bind = ( . . . args : unknown [ ] ) = > handlePush . $ $vencordOriginal . bind ( . . . args ) ;
handlePush . bind = ( . . . args : unknown [ ] ) = > handlePush . $ $vencordOriginal . bind ( . . . args ) ;
Object . defineProperty ( webpackGlobal , "push" , {
Object . defineProperty ( webpackGlobal , "push" , {
configurable : true ,
get : ( ) = > handlePush ,
get : ( ) = > handlePush ,
set ( v ) {
set ( v ) {
handlePush . $ $vencordOriginal = v ;
handlePush . $ $vencordOriginal = v ;
} ,
}
configurable : true
} ) ;
} ) ;
}
}
function patchFactories ( factories : Record < string | number , ( module : { exports : any ; } , exports : any , require : any ) = > void > ) {
let webpackNotInitializedLogged = false ;
const { subscriptions , listeners } = Vencord . Webpack ;
const { patches } = Vencord . Plugins ;
function patchFactories ( factories : Record < string , ( module : any , exports : any , require : WebpackInstance ) = > void > ) {
for ( const id in factories ) {
for ( const id in factories ) {
let mod = factories [ id ] ;
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.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code : string = "0," + mod . toString ( ) . replaceAll ( "\n" , "" ) ;
const originalMod = mod ;
const originalMod = mod ;
const patchedBy = new Set ( ) ;
const patchedBy = new Set ( ) ;
const factory = factories [ id ] = function ( module , exports , require ) {
const factory = factories [ id ] = function ( module : any , exports : any , require : WebpackInstance ) {
if ( wreq == null && IS_DEV ) {
if ( ! webpackNotInitializedLogged ) {
webpackNotInitializedLogged = true ;
logger . error ( "WebpackRequire was not initialized, running modules without patches instead." ) ;
}
return void originalMod ( module , exports , require ) ;
}
try {
try {
mod ( module , exports , require ) ;
mod ( module , exports , require ) ;
} catch ( err ) {
} catch ( err ) {
// Just rethrow discord errors
// Just rethrow discord errors
if ( mod === originalMod ) throw err ;
if ( mod === originalMod ) throw err ;
logger . error ( "Error in patched chunk" , err ) ;
logger . error ( "Error in patched module ", err ) ;
return void originalMod ( module , exports , require ) ;
return void originalMod ( module , exports , require ) ;
}
}
@ -153,11 +208,11 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
return ;
return ;
}
}
for ( const callback of l isteners) {
for ( const callback of module L isteners) {
try {
try {
callback ( exports , id ) ;
callback ( exports , id ) ;
} catch ( err ) {
} catch ( err ) {
logger . error ( "Error in webpack listener", err ) ;
logger . error ( "Error in Webpack module listener:\n", err , callback ) ;
}
}
}
}
@ -171,30 +226,48 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
callback ( exports . default , id ) ;
callback ( exports . default , id ) ;
}
}
} catch ( err ) {
} catch ( err ) {
logger . error ( "Error while firing callback for webpack chunk", err ) ;
logger . error ( "Error while firing callback for Webpack subscription:\n", err , filter , callback ) ;
}
}
}
}
} as any as { toString : ( ) = > string , original : any , ( . . . args : any [ ] ) : void ; } ;
} as any as { toString : ( ) = > string , original : any , ( . . . args : any [ ] ) : void ; } ;
// for some reason throws some error on which calling .toString() leads to infinite recursion
factory . toString = originalMod . toString . bind ( originalMod ) ;
// when you force load all chunks???
factory . toString = ( ) = > mod . toString ( ) ;
factory . original = originalMod ;
factory . original = originalMod ;
for ( const factoryListener of factoryListeners ) {
try {
factoryListener ( originalMod ) ;
} catch ( err ) {
logger . error ( "Error in Webpack factory listener:\n" , err , factoryListener ) ;
}
}
// 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.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code : string = "0," + mod . toString ( ) . replaceAll ( "\n" , "" ) ;
for ( let i = 0 ; i < patches . length ; i ++ ) {
for ( let i = 0 ; i < patches . length ; i ++ ) {
const patch = patches [ 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 ( patch . predicate && ! patch . predicate ( ) ) continue ;
if ( ! code . includes ( patch . find ) ) continue ;
if ( code . includes ( patch . find ) ) {
patchedBy . add ( patch . plugin ) ;
patchedBy . add ( patch . plugin ) ;
const executePatch = traceFunction ( ` patch by ${ patch . plugin } ` , ( match : string | RegExp , replace : string ) = > code . replace ( match , replace ) ) ;
const previousMod = mod ;
const previousMod = mod ;
const previousCode = code ;
const previousCode = code ;
// we change all patch.replacement to array in plugins/index
// W e change all patch.replacement to array in plugins/index
for ( const replacement of patch . replacement as PatchReplacement [ ] ) {
for ( const replacement of patch . replacement as PatchReplacement [ ] ) {
if ( replacement . predicate && ! replacement . predicate ( ) ) continue ;
if ( replacement . predicate && ! replacement . predicate ( ) ) continue ;
const lastMod = mod ;
const lastMod = mod ;
const lastCode = code ;
const lastCode = code ;
@ -212,15 +285,17 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
if ( patch . group ) {
if ( patch . group ) {
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } had no effect ` ) ;
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } had no effect ` ) ;
code = previousCode ;
mod = previousMod ;
mod = previousMod ;
code = previousCode ;
patchedBy . delete ( patch . plugin ) ;
patchedBy . delete ( patch . plugin ) ;
break ;
break ;
}
}
} else {
continue ;
}
code = newCode ;
code = newCode ;
mod = ( 0 , eval ) ( ` // Webpack Module ${ id } - Patched by ${ [ . . . patchedBy ] . join ( ", " ) } \ n ${ newCode } \ n//# sourceURL=WebpackModule ${ id } ` ) ;
mod = ( 0 , eval ) ( ` // Webpack Module ${ id } - Patched by ${ [ . . . patchedBy ] . join ( ", " ) } \ n ${ newCode } \ n//# sourceURL=WebpackModule ${ id } ` ) ;
}
} catch ( err ) {
} catch ( err ) {
logger . error ( ` Patch by ${ patch . plugin } errored (Module id is ${ id } ): ${ replacement . match } \ n ` , err ) ;
logger . error ( ` Patch by ${ patch . plugin } errored (Module id is ${ id } ): ${ replacement . match } \ n ` , err ) ;
@ -258,15 +333,16 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
}
}
patchedBy . delete ( patch . plugin ) ;
patchedBy . delete ( patch . plugin ) ;
if ( patch . group ) {
if ( patch . group ) {
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } errored ` ) ;
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } errored ` ) ;
code = previousCode ;
mod = previousMod ;
mod = previousMod ;
code = previousCode ;
break ;
break ;
}
}
code = lastCode ;
mod = lastMod ;
mod = lastMod ;
code = lastCode ;
}
}
}
}
@ -274,4 +350,3 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
}
}
}
}
}
}
}