Webpack Patcher

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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);
})();