Webpack Patcher

Helper script to patch the code of webpack modules at runtime. Exposes a global WebpackPatcher object.

// ==UserScript==
// @name        Webpack Patcher
// @description Helper script to patch the code of webpack modules at runtime. Exposes a global WebpackPatcher object.
// @author      bertigert
// @version     2.0.2
// @icon        https://www.google.com/s2/favicons?sz=64&domain=webpack.js.org
// @namespace   Webpack Patcher
// @match       http*://*/*
// @grant       none
// @run-at      document-start
// ==/UserScript==

(function() {
    "use strict";
    
    // JSDoc was overhauled by AI
    
    class Logger {
        constructor(prefix, log_level = "log") {
            this.prefix = prefix;
            this.log_levels = {
                debug: 0,
                log: 1,
                warn: 2,
                error: 3
            };
            this.current_level = this.log_levels[log_level] == null ? this.log_levels.log : this.log_levels[log_level];
        }

        debug(...args) { 
            if (this.current_level <= this.log_levels.debug) {
                console.debug(this.prefix, ...args); 
            }
        }
        
        log(...args) { 
            if (this.current_level <= this.log_levels.log) {
                console.log(this.prefix, ...args); 
            }
        }
        
        warn(...args) { 
            if (this.current_level <= this.log_levels.warn) {
                console.warn(this.prefix, ...args); 
            }
        }
        
        error(...args) { 
            if (this.current_level <= this.log_levels.error) {
                console.error(this.prefix, ...args); 
            }
        }
    }

    /**
     * Class to patch webpack modules by intercepting module factory registration.
     * Works by hooking Function.prototype to catch webpack's module initialization.
     */
    class WebpackPatcher {
        static VERSION = "2.0.0";
        
        static SYM_PROXY_INNER_GET = Symbol("WebpackPatcher.proxyInnerGet");
        static SYM_PROXY_INNER_VALUE = Symbol("WebpackPatcher.proxyInnerValue");
        static SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory");

        /**
         * @param {Object} logger - Logger instance for debug/error output
         * @param {Object} options - Configuration options
         * @param {boolean} options.enable_cache - Enable caching for performance (default: false). Is only useful in some cases, check yourself.
         * @param {boolean} options.use_eval - Use eval instead of new Function for better debugging (default: true)
         * @param {Function} options.on_detect - Callback when webpack is detected (default: null)
         * @param {Function} options.filter_func - Additional filter function: (webpack_require, stack_lines) => boolean. Should return true to allow the module, false to reject it.
         * @param {Object} options.webpack_property_names - Property names to hook: {modules: "m", cache: "c"} (default: {modules: "m", cache: "c"})
         */
        constructor(logger, options = {}) {
            this.patches = [];
            this.patched_modules = new Set();
            this.module_registration_count = new Map();
            this.module_factories = null;
            this.webpack_require = null;
            this.webpack_cache = null;
            this.hooked = false;
            this.logger = logger || window.console;
            
            // default options
            this.cache_enabled = options.enable_cache !== undefined ? options.enable_cache : false;
            this.use_eval = options.use_eval !== undefined ? options.use_eval : true;
            this.on_detect = options.on_detect || null;
            this.filter_func = options.filter_func || null;
            this.webpack_property_names = options.webpack_property_names || { modules: "m", cache: "c" };
            
            this.factory_string_cache = this.cache_enabled ? new Map() : null;
            this.registrars = {}; // Store registrar objects by name
            
            this.placeholder_id = Math.random().toString(36).substring(2, 10); // just to make sure it's unique enough
            this.placeholders = Object.freeze({
                self: `WEBPACKPATCHER_PLACEHOLDER_SELF_${this.placeholder_id}`,
                functions: `WEBPACKPATCHER_PLACEHOLDER_FUNCTIONS_${this.placeholder_id}`,
                data: `WEBPACKPATCHER_PLACEHOLDER_DATA_${this.placeholder_id}`
            });

            this.event_listeners = {
                webpack_detected: [],
                module_registered: [],
                module_patched: []
            };
        }

        /**
         * Get placeholder replacements for a given registrar name
         * @param {string} registrar_name - Name of the registrar
         * @returns {Object} Object mapping placeholders to their replacements
         */
        _get_placeholder_replacements(registrar_name) {
            return {
                [this.placeholders.self]: `window.WebpackPatcher.Registrars["${registrar_name}"]`,
                [this.placeholders.functions]: `window.WebpackPatcher.Registrars["${registrar_name}"].functions`,
                [this.placeholders.data]: `window.WebpackPatcher.Registrars["${registrar_name}"].data`
            };
        }

        /**
         * Add an event listener
         * @param {string} event - Event name: "webpack_detected", "module_registered", "module_patched"
         * @param {Function} callback - Callback function
         */
        add_event_listener(event, callback) {
            if (!this.event_listeners[event]) {
                this.logger.warn(`Unknown event: ${event}`);
                return;
            }
            this.event_listeners[event].push(callback);
            this.logger.debug(`Added event listener for: ${event}`);
        }

        /**
         * Remove an event listener
         * @param {string} event - Event name
         * @param {Function} callback - Callback function to remove
         */
        remove_event_listener(event, callback) {
            if (!this.event_listeners[event]) {
                return;
            }
            const index = this.event_listeners[event].indexOf(callback);
            if (index > -1) {
                this.event_listeners[event].splice(index, 1);
                this.logger.debug(`Removed event listener for: ${event}`);
            }
        }

        /**
         * Emit an event to all listeners
         * @param {string} event - Event name
         * @param {...any} args - Arguments to pass to listeners
         * @private
         */
        _emit_event(event, ...args) {
            if (!this.event_listeners[event]) {
                return;
            }
            for (const listener of this.event_listeners[event]) {
                try {
                    listener(...args);
                } catch (e) {
                    this.logger.error(`Error in ${event} event listener:`, e);
                }
            }
        }

        /**
         * Check if a value matches an identifier (supports both strings and RegExp)
         * @param {string} value - Value to test
         * @param {string|RegExp} identifier - Pattern to match (substring or regex)
         * @returns {boolean} True if value matches identifier
         */
        _matches_identifier(value, identifier) {
            if (typeof identifier === 'string') {
                return value.includes(identifier);
            } else if (identifier instanceof RegExp) {
                return identifier.test(value);
            }
            return false;
        }

        /**
         * Register patches to be applied when modules are loaded
         * @param {Object} options - Registration options
         * @param {string} options.name - Name of the registrar (used to group patches and create user object)
         * @param {Object} [options.data] - Initial data object for the registrar
         * @param {Object} [options.functions] - Initial functions object for the registrar
         * @param {Array<Object>} patches - Array of patch configurations
         * @param {Object} [existing_registrar] - Existing registrar object to reuse (for buffer flushing)
         * @returns {Object} Registrar object with data and functions properties
         */
        register_patches(options, patches, existing_registrar = null) {
            const registrar_name = options.name;
            
            if (!registrar_name) {
                throw new Error("Registrar name is required");
            }

            if (!window.WebpackPatcher.Registrars[registrar_name]) {
                const registrar_obj = existing_registrar || {
                    data: options.data || {},
                    functions: options.functions || {}
                };
                
                Object.defineProperty(window.WebpackPatcher.Registrars, registrar_name, {
                    value: registrar_obj,
                    writable: false,
                    enumerable: true,
                    configurable: false
                });
                
                this.registrars[registrar_name] = registrar_obj;
                
                this.logger.debug(`Created new registrar: ${registrar_name}`);
            }

            this.patches.push(...patches.map(p => ({ ...p, _registrar_name: registrar_name })));
            
            this.logger.debug(`Registered ${patches.length} patch(es) for ${registrar_name}, total patches: ${this.patches.length}`);

            if (!this.hooked) {
                this.hook_webpack();
            }

            return window.WebpackPatcher.Registrars[registrar_name];
        }

        /**
         * Hook into webpack by intercepting Function.prototype property setters
         */
        hook_webpack() {
            if (this.hooked) return;

            const self = this;
            const original_define_property = Object.defineProperty;
            let webpack_detected = false;
            let cache_detected = false;

            original_define_property(Function.prototype, this.webpack_property_names.cache, {
                enumerable: false,
                configurable: true,
                set: function(cache_obj) {
                    original_define_property(this, self.webpack_property_names.cache, {
                        value: cache_obj,
                        writable: true,
                        configurable: true
                    });
                    
                    if (cache_detected) {
                        self.logger.debug("Cache already detected, skipping duplicate initialization");
                        return;
                    }

                    if (!String(this).includes("exports:{}")) { // default filter which should apply to every site
                        return;
                    }

                    const stack = new Error().stack;
                    const stack_lines = stack?.split('\n') || [];

                    if (self.filter_func && !self.filter_func(this, stack_lines)) {
                        return;
                    }

                    cache_detected = true;

                    self.webpack_cache = cache_obj;
                    self.logger.debug("Captured webpack cache object");
                },
                get: function() {
                    return self.webpack_cache;
                }
            });

            // catch webpack module factory assignment
            original_define_property(Function.prototype, this.webpack_property_names.modules, {
                enumerable: false,
                configurable: true,
                set: function(module_factories) {
                    original_define_property(this, self.webpack_property_names.modules, { // restore original property
                        value: module_factories,
                        writable: true,
                        configurable: true
                    });

                    if (webpack_detected) {
                        // self.logger.debug("Webpack already detected, skipping duplicate initialization");
                        return;
                    }

                    if (!String(this).includes("exports:{}")) { // default filter which should apply to every site
                        return;
                    }

                    const stack = new Error().stack;
                    const stack_lines = stack?.split('\n') || [];

                    if (self.filter_func && !self.filter_func(this, stack_lines)) {
                        return;
                    }

                    webpack_detected = true;

                    const module_count = Object.keys(module_factories).length;
                    self.logger.debug(`Detected webpack module factory assignment (with ${module_count} modules)`);
                    self.webpack_require = this;

                    self._emit_event('webpack_detected', this, module_factories);

                    if (self.on_detect) {
                        try {
                            self.on_detect(this, module_factories);
                        } catch (e) {
                            self.logger.error("Error in on_detect callback:", e);
                        }
                    }

                    // intercept new factory registrations
                    const proxied_factories = new Proxy(module_factories, {
                        set(target, module_id, factory) {
                            // Track registration count
                            // const count = (self.module_registration_count.get(module_id) || 0) + 1;
                            // self.module_registration_count.set(module_id, count);
                            
                            // if (count > 1) {
                            //     self.logger.warn(`Module ${module_id} registered ${count} times (possible HMR or duplicate registration)`);
                            // }

                            self._emit_event('module_registered', module_id, factory);

                            // intercept execution using helper
                            const factory_proxy = self._create_factory_proxy(module_id, factory);
                            
                            target[module_id] = factory_proxy;
                            return true;
                        },
                        

                        get(target, prop, receiver) {
                            const value = Reflect.get(target, prop, receiver);
                            

                            // if the value is a proxied factory, return the inner value for direct access
                            if (value?.[WebpackPatcher.SYM_PROXY_INNER_GET]) {
                                return value[WebpackPatcher.SYM_PROXY_INNER_VALUE];
                            }
                            

                            return value;
                        }
                    });

                    self.module_factories = module_factories;
                    
                    original_define_property(this, self.webpack_property_names.modules, {
                        value: proxied_factories,
                        writable: true,
                        configurable: true
                    });

                    // wrap all pre-existing modules.
                    // i don't think we need to worry about the webpack cache here, as it should be empty at this point
                    for (const module_id in module_factories) {
                        const factory = module_factories[module_id];
                        const factory_proxy = self._create_factory_proxy(module_id, factory);
                        module_factories[module_id] = factory_proxy;
                    }
                    
                    self.logger.debug(`Wrapped ${module_count} pre-existing modules in factory proxies`);
                }
            });

            this.hooked = true;
            this.logger.debug("Webpack hooking initialized");
        }

        /**
         * Get cached or compute factory string
         * @param {string} module_id - Module ID for cache key
         * @param {Function} factory - Factory function
         * @returns {string} Factory as string
         */
        _get_factory_string(module_id, factory) {
            if (this.cache_enabled && this.factory_string_cache.has(module_id)) {
                this.logger.debug(`Using cached factory string for module ${module_id} (cache hit)`);
                return this.factory_string_cache.get(module_id);
            }
            
            const factory_str = factory.toString();
            
            if (this.cache_enabled) {
                this.factory_string_cache.set(module_id, factory_str);
            }
            
            return factory_str;
        }

        /**
         * Check if module matches pattern
         * @param {string} factory_str - Factory string
         * @param {Object} patch - Patch configuration
         * @returns {boolean} True if matches
         */
        _check_pattern_match(factory_str, patch) {
            const { find } = patch;
            const finds = Array.isArray(find) ? find : [find];
            
            return finds.some(pattern => this._matches_identifier(factory_str, pattern));
        }

        /**
         * Get the patched factory for a module, patching it lazily if needed
         * @param {string} module_id - Module ID
         * @param {Function} factory - Original factory function
         * @returns {Function} Patched factory or original if no patches match
         */
        _get_or_patch_factory(module_id, factory) {
            if (factory[WebpackPatcher.SYM_ORIGINAL_FACTORY] != null) {
                return factory;
            }
            
            if (this.patched_modules.has(module_id)) {
                return factory;
            }

            const factory_str = this._get_factory_string(module_id, factory);
            let current_factory = factory;
            let current_factory_str = factory_str;
            let any_patches_applied = false;
            
            for (let i = 0; i < this.patches.length; i++) {
                const patch = this.patches[i];
                
                if (!this._check_pattern_match(current_factory_str, patch)) {
                    continue;
                }

                // this.logger.debug(`Module ${module_id} matches patch ${i + 1}/${this.patches.length}`);
                
                const patch_result = this._apply_patch(current_factory, current_factory_str, patch.replacements, module_id, patch._registrar_name);
                
                if (patch_result.factory !== current_factory) {
                    current_factory = patch_result.factory;
                    current_factory_str = patch_result.factory_str;
                    any_patches_applied = true;
                    
                    // this.logger.debug(`Applied patch ${i + 1} to module ${module_id}`);
                }
            }
            
            if (any_patches_applied) {
                current_factory[WebpackPatcher.SYM_ORIGINAL_FACTORY] = factory;
                this.module_factories[module_id] = current_factory;
                this._emit_event('module_patched', module_id, current_factory, factory);
            }
            
            this.patched_modules.add(module_id);
            return current_factory;
        }

        /**
         * Apply patch using matches_and_replacements array format
         * @param {Function} factory - Original factory function
         * @param {string} factory_str - Factory as string
         * @param {Array<Object>} matches_and_replacements - Array of {match, replace, global} objects
         * @param {string} module_id - Module ID for logging
         * @param {string} registrar_name - Name of registrar for placeholder replacement
         * @returns {{factory: Function, factory_str: string}} Patched factory and its string representation, or original if patching fails
         */
        _apply_patch(factory, factory_str, matches_and_replacements, module_id, registrar_name) {
            let patched_code;
            
            if (!Array.isArray(matches_and_replacements) || matches_and_replacements.length === 0) {
                this.logger.warn(`Module was matched, but no replacements provided`);
                return { factory, factory_str };
            }

            try {
                patched_code = factory_str;
                let total_replacements = 0;

                for (let i = 0; i < matches_and_replacements.length; i++) {
                    const match_and_replacement = matches_and_replacements[i];
                    let replacement_occurred = false;

                    const { match, replace, global } = match_and_replacement;
                    const func = global || (match instanceof RegExp && match.global) ? "replaceAll" : "replace";
                    
                    if (typeof replace === 'function') {
                        patched_code = patched_code[func](match, (...args) => {
                            replacement_occurred = true;
                            return replace(...args);
                        });
                    } else {
                        patched_code = patched_code[func](match, (...args) => {
                            replacement_occurred = true;
                            return replace;
                        });
                    }

                    if (replacement_occurred) {
                        total_replacements++;
                    } else {
                        this.logger.warn(`Replacement ${i + 1}/${matches_and_replacements.length} skipped (no match) for module ${module_id}`);
                    }
                }

                if (total_replacements === 0) {
                    this.logger.warn(`No replacements occurred in module ${module_id}, returning original factory`);
                    return { factory, factory_str };
                }

                const placeholder_replacements = this._get_placeholder_replacements(registrar_name);
                for (const [placeholder, replacement] of Object.entries(placeholder_replacements)) {
                    patched_code = patched_code.replaceAll(placeholder, replacement);
                }

                // add source map
                const patched_source = `// Webpack Module ${module_id} - Patched by WebpackPatcher\n0,${patched_code}\n//# sourceURL=WebpackModule${module_id}`;
                
                const patched_factory = this.use_eval 
                    ? (0, eval)(patched_source)
                    : new Function(`return (${patched_code})`)();
                
                return { 
                    factory: patched_factory, 
                    factory_str: patched_code 
                };
            } catch (e) {
                this.logger.error(`Replacement based patching failed:`, e, "patched code:", patched_code);
                return { factory, factory_str };
            }
        }

        /**
         * @param {Object} logger - Logger instance for debug/error output
         * @param {Object} options - Configuration options
         * @param {boolean} options.enable_cache - Enable caching for performance (default: false). Is only useful in some cases, check yourself.
         * @param {boolean} options.use_eval - Use eval instead of new Function for better debugging. Can be disabled by sites though. (default: true)
         * @param {Function} options.on_detect - Callback when webpack is detected (default: null)
         * @param {Function} options.filter_func - Filter function to select webpack instance: (webpack_require, stack_lines) => boolean. Should return true to allow the instance, false to reject it.
         * @param {Object} options.webpack_property_names - Property names to hook: {modules: "m", cache: "c"} (default: {modules: "m", cache: "c"})
         */
        static initialize(logger, options={}) {
            const patcher = new WebpackPatcher(logger, options);
            patcher.hook_webpack();
            logger.log("WebpackPatcher initialized, options:", options);
            return patcher;
        }

        /**
         * Clear caches
         * @param {string} module_id - Optional specific module ID to clear
         */
        clear_cache(module_id=null) {
            if (!this.cache_enabled) {
                this.logger.warn("Cache is disabled, nothing to clear");
                return;
            }
            
            if (module_id) {
                this.factory_string_cache.delete(module_id);
            } else {
                this.factory_string_cache.clear();
            }
        }

        /**
         * Create a factory proxy that intercepts execution for lazy patching
         * @param {string} module_id - Module ID
         * @param {Function} factory - Original factory function
         * @returns {Proxy} Proxied factory
         * @private
         */
        _create_factory_proxy(module_id, factory) {
            const self = this;
            return new Proxy(factory, {
                apply(factory_target, thisArg, argArray) {
                    const patched_factory = self._get_or_patch_factory(module_id, factory_target);
                    return patched_factory.apply(thisArg, argArray);
                },
                
                get(factory_target, prop, receiver) {
                    return self._handle_factory_get(factory_target, prop, receiver);
                }
            });
        }

        /**
         * Handle get trap for factory proxies
         * @param {Function} factory_target - Target factory function
         * @param {string|symbol} prop - Property being accessed
         * @param {any} receiver - Proxy receiver
         * @returns {any} Property value
         * @private
         */
        _handle_factory_get(factory_target, prop, receiver) {
            if (prop === WebpackPatcher.SYM_PROXY_INNER_GET) {
                return true;
            }
            if (prop === WebpackPatcher.SYM_PROXY_INNER_VALUE) {
                return factory_target;
            }
            
            const actual_factory = factory_target[WebpackPatcher.SYM_ORIGINAL_FACTORY] || factory_target;
            
            // ensure toString has correct `this` context
            if (prop === "toString") {
                return actual_factory.toString.bind(actual_factory);
            }
            
            return Reflect.get(actual_factory, prop, receiver);
        }
    }

    /**
     * Helper class for registering patches with buffering support.
     * Allows patches to be registered before the patcher is initialized.
     */
    class WebpackPatchRegistrar {
        /**
         * @param {WebpackPatcher|null} patcher - Patcher instance (null if not yet initialized)
         */
        constructor(patcher = null) {
            this.patcher = patcher;
            this.patch_buffer = [];
            this.event_listener_buffer = []; // Buffer for event listeners registered before patcher initialization
            this.is_flushing = false;
        }

        /**
         * Set the patcher instance and flush buffered patches
         * @param {WebpackPatcher} patcher - Patcher instance
         */
        set_patcher(patcher) {
            if (this.patcher) {
                patcher.logger.warn("Patcher already set, ignoring duplicate initialization");
                return;
            }
            
            this.patcher = patcher;
            this._flush_buffers();
        }

        /**
         * Flush all buffered patches and event listeners to the patcher
         * @private
         */
        _flush_buffers() {
            if (this.is_flushing) {
                return;
            }
            
            this.is_flushing = true;
            
            if (this.patch_buffer.length > 0) {
                this.patcher.logger.log(`Flushing ${this.patch_buffer.length} buffered patch(es) to patcher`);
                
                for (const buffered of this.patch_buffer) {
                    this.patcher.register_patches(buffered.options, buffered.patches, buffered.registrar);
                }
                
                this.patch_buffer = [];
                this.patcher.logger.debug("Patch buffer flushed successfully");
            }
            
            if (this.event_listener_buffer.length > 0) {
                this.patcher.logger.log(`Flushing ${this.event_listener_buffer.length} buffered event listener(s) to patcher`);
                
                for (const buffered of this.event_listener_buffer) {
                    this.patcher.add_event_listener(buffered.event, buffered.callback);
                }
                
                this.event_listener_buffer = [];
                this.patcher.logger.debug("Event listener buffer flushed successfully");
            }
            
            this.is_flushing = false;
        }

        /**
         * Register patches with the following structure:
         * 
         * @param {Object} options - Registration options
         * @param {string} options.name - Name of the registrar (creates/gets object at WebpackPatcher.Registrars[name])
         * @param {Object} [options.data] - Initial data object for the registrar
         * @param {Object} [options.functions] - Initial functions object for the registrar
         * @param {Array<Object>} patches - Array of patch configurations
         * @returns {Object} Registrar object with {data: {}, functions: {}} for user to populate
         * 
         * @example
         * WebpackPatcher.register_patches(
         *   {
         *       name: string, // required, creates/gets WebpackPatcher.Registrars[name]
         *       data: object, // initial data object for the registrar
         *       functions: object // initial functions object for the registrar
         *   },
         *   [
         *       {
         *           find: string | RegExp | Array<string|RegExp>, // substring or regex to match in module code
         *           replacements: [
         *               {
         *                   match: string | RegExp, // substring or regex to match
         *                   replace: string | Function, // replacement string or function (function receives same args as String.replace)
         *                   global: boolean // optional, default false, if true uses replaceAll
         *               }
         *           ]
         *       }
         *   ]
         * );
         */
        register_patches(options, patches) {
            if (this.patcher) {
                return this.patcher.register_patches(options, patches);
            } else {
                // keep reference to this object for flushing later
                const registrar_obj = {
                    data: options.data || {},
                    functions: options.functions || {}
                };
                
                this.patch_buffer.push({ 
                    options, 
                    patches, 
                    registrar: registrar_obj
                });
                console.debug("[WebpackPatcher]", `Buffered ${patches.length} patch(es) for "${options.name}" (patcher not yet initialized)`);
                
                return registrar_obj;
            }
        }

        /**
         * Add an event listener for webpack events
         * @param {string} event - Event name: "webpack_detected", "module_registered", "module_patched"
         * @param {Function} callback - Callback function
         * 
         * Events:
         * - webpack_detected: (webpack_require, module_factories) => void
         * - module_registered: (module_id, factory) => void
         * - module_patched: (module_id, patched_factory, original_factory) => void
         * 
         * @example
         * WebpackPatcher.addEventListener('webpack_detected', (wreq, factories) => {
         *     console.log('Webpack detected!', wreq);
         * });
         * 
         * @example
         * WebpackPatcher.addEventListener('module_registered', (module_id, factory) => {
         *     console.log('Module registered:', module_id);
         * });
         * 
         * @example
         * WebpackPatcher.addEventListener('module_patched', (module_id, patched, original) => {
         *     console.log('Module patched:', module_id);
         * });
         */
        add_event_listener(event, callback) {
            if (this.patcher) {
                this.patcher.add_event_listener(event, callback);
            } else {
                this.event_listener_buffer.push({ event, callback });
                console.debug("[WebpackPatcher]", `Buffered event listener for '${event}' (patcher not yet initialized, total buffered: ${this.event_listener_buffer.length})`);
            }
        }

        /**
         * Remove an event listener
         * @param {string} event - Event name
         * @param {Function} callback - Callback function to remove
         */
        remove_event_listener(event, callback) {
            if (this.patcher) {
                this.patcher.remove_event_listener(event, callback);
            }
        }


        _Webpack_cache = {
            get: (module_id) => {
                return this.patcher?.webpack_cache?.[module_id] || null;
            },
            getAll: () => {
                return this.patcher?.webpack_cache ? { ...this.patcher.webpack_cache } : null;
            },
            delete: (module_id) => {
                if (this.patcher?.webpack_cache) {
                    delete this.patcher.webpack_cache[module_id];
                }
            },
            set: (module_id, value) => {
                if (this.patcher?.webpack_cache) {
                    this.patcher.webpack_cache[module_id] = value;
                }
            }
        }

        /**
         * Get the webpack require function (if detected)
         * @returns {Function|null} Webpack require function or null
         */
        get webpack_require() {return this.patcher?.webpack_require || null;}

        /**
         * Get the webpack module factories object (if detected)
         * @returns {Object|null} Module factories or null
         */
        get module_factories() {return this.patcher?.module_factories || null;}

        /**
         * Get the webpack cache object (if detected)
         * @returns {Object|null} Webpack cache or null
         */
        get webpack_cache() {return this._Webpack_cache};

        /**
         * Get all registered patches
         * @returns {Array} Array of patch configurations
         */
        get patches() {return this.patcher?.patches || [];}

        /**
         * Get set of patched module IDs
         * @returns {Set} Set of module IDs that have been patched
         */
        get patched_modules() {return this.patcher?.patched_modules || new Set();}

        /**
         * Check if webpack has been detected
         * @returns {boolean} True if webpack is detected
         */
        get is_webpack_detected() {return this.patcher?.webpack_require != null;}
    }


    function main(CONFIGURATIONS) {
        const logger = new Logger("[WebpackPatcher]", "debug");

        const matching_configuration = CONFIGURATIONS.find(config => config.site_match());

        if (matching_configuration) {
            const webpack_patch_registrar = new WebpackPatchRegistrar(null);

            Object.defineProperties(window, {
                WebpackPatcher: {
                    value: Object.freeze({
                        register: Object.freeze(webpack_patch_registrar.register_patches.bind(webpack_patch_registrar)),
                        addEventListener: Object.freeze(webpack_patch_registrar.add_event_listener.bind(webpack_patch_registrar)),
                        removeEventListener: Object.freeze(webpack_patch_registrar.remove_event_listener.bind(webpack_patch_registrar)),
                        
                        Registrars: {},

                        get webpackRequire() {
                            return webpack_patch_registrar.webpack_require;
                        },
                        get moduleFactories() {
                            return webpack_patch_registrar.module_factories;
                        },
                        get moduleCache() {
                            return webpack_patch_registrar.webpack_cache;
                        },
                        get patches() {
                            return webpack_patch_registrar.patches;
                        },
                        get patchedModules() {
                            return webpack_patch_registrar.patched_modules;
                        },
                        get isWebpackDetected() {
                            return webpack_patch_registrar.is_webpack_detected;
                        },
                        get placeholders() {
                            return webpack_patch_registrar.patcher?.placeholders;
                        },
                        get VERSION() {
                            return WebpackPatcher.VERSION;
                        }
                    }),
                    writable: false,
                    configurable: false
                }
            });

            logger.log(`Using configuration for ${location.hostname}`);
            const patcher = WebpackPatcher.initialize(logger, matching_configuration.options || {});
            webpack_patch_registrar.set_patcher(patcher);
        }   
    }

    const CONFIGURATIONS = [
        {
            site_match: () => location.hostname === "www.deezer.com" || location.href.includes("deezer-desktop/resources/app.asar/build/index.html"),
            options: {
                filter_func: (ctx, stack_lines) => {
                    return /\/cache\/js\/runtime\..*?\.js(?::[0-9]+:[0-9]+)?$/.test(stack_lines[stack_lines.length-1]);
                },
            },
        },
        {
            site_match: () => location.hostname === "discord.com",
            options: {
                filter_func: (ctx, stack_lines) => {
                    return /https:\/\/discord\.com\/assets\/web\..*?\.js/.test(stack_lines[stack_lines.length-1]);
                },
            },
        }
    ];

    main(CONFIGURATIONS);
})();