Webpack Patcher

Library script to patch the code of webpack modules at runtime. Exposes a global register_webpack_patches function.

La data de 27-08-2025. Vezi ultima versiune.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Webpack Patcher
// @description Library script to patch the code of webpack modules at runtime. Exposes a global register_webpack_patches function.
// @author      bertigert
// @version     1.0.0
// @icon        https://www.google.com/s2/favicons?sz=64&domain=
// @namespace   Violentmonkey Scripts
// @match       https://www.deezer.com/us/*
// @grant       none
// @run-at      document-start
// ==/UserScript==


(function() {
    "use strict";
    /**
     * Class to patch webpack modules by hooking into the webpack global object array push
     *
     * **It is important that the patcher is initialized as early as possible to catch modules as they are loaded**
     */
    class WebpackPatcher {
        static VERSION = 1;

        constructor(global_obj_key, logger, benchmark=false) {
            this.global_obj = window[global_obj_key];
            this.patch_targets = new Map();
            this.patched_modules = new Set();
            this.patch_counts = new Map();
            this.hooked = false;
            this.benchmark = benchmark;
            this.logger = logger || window.console;
            this._module_cache = new Map();
            this._function_string_cache = new Map();
        }

        /**
         * Find a module by its ID in the webpack modules array
         * @param {number} id - The webpack module ID to find
         * @returns {Array|null} - The module array [ids, exports, ...] or null if not found
        */
        get_module(id) {
            if (this._module_cache.has(id)) {
                return this._module_cache.get(id);
            }

            for (const e of this.global_obj) {
                if (e[0].some(mod_id => mod_id == id)) {
                    this._module_cache.set(id, e);
                    return e;
                }
            }
            return null;
        }

        /**
         * Register a module to be patched when it becomes available. Should be called as early as possible.
         * @param {number} module_id - The webpack module ID to target
         * @param {Array} methods - Array of method patch configurations from PATCHES structure
         */
        register_patch(module_id, methods) {
            // Merge with existing patches for this module
            if (this.patch_targets.has(module_id)) {
                const existing_methods = this.patch_targets.get(module_id);
                this.patch_targets.set(module_id, [...existing_methods, ...methods]);
            } else {
                this.patch_targets.set(module_id, methods);
                this.patch_counts.set(module_id, 0);
            }

            const existing_module = this.get_module(module_id);
            if (existing_module && methods.length > 0) {
                this.patch_module(existing_module, module_id, methods);
            }

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

        /**
         * Register multiple patches at once. Should be called as early as possible.
         * @param {Object} patches - The PATCHES structure mapping module IDs to patch configurations
         */
        register_patches(patches) {
            Object.entries(patches).forEach(([id, {methods}]) => {
                this.register_patch(parseInt(id), methods);
            });
        }

        /**
         * Patch a module using the PATCHES structure
         * @param {*} module
         * @param {number} module_id
         * @param {Array} methods - Array of method patch configurations
         * @returns true if a method was patched, false otherwise
         */
        patch_module(module, module_id, methods) {
            const start_time = this.benchmark ? performance.now() : 0;
            try {
                let patched_count = 0;
                const module_exports = module[1];
                const current_patch_count = this.patch_counts.get(module_id) || 0;

                for (const [name, method] of Object.entries(module_exports)) {
                    if (typeof method !== 'function') {
                        continue;
                    }

                    if (current_patch_count + patched_count >= this.patch_targets.get(module_id).length) {
                        break;
                    }

                    const matching_method = methods.find(({identifiers}) =>
                        identifiers.every(identifier => this.does_method_match(method, identifier, module_id, name))
                    );

                    if (matching_method) {
                        const patch_start = this.benchmark ? performance.now() : 0;
                        const patched_method = this.patch_method_with_regexp_or_raw_string(
                            method,
                            matching_method.matches_and_replacements,
                            module_id,
                            name
                        );
                        const patch_end = this.benchmark ? performance.now() : 0;

                        module_exports[name] = patched_method;

                        if (this.benchmark) {
                            this.logger.debug(`Patched method '${name}' in module ${module_id} (${(patch_end - patch_start).toFixed(2)}ms)`);
                        } else {
                            this.logger.debug(`Patched method '${name}' in module ${module_id}`);
                        }
                        patched_count++;
                    }
                }

                if (patched_count > 0) {
                    this.patched_modules.add(module_id);
                    this.patch_counts.set(module_id, current_patch_count + patched_count);

                    const all_methods = this.patch_targets.get(module_id);
                    if (this.patch_counts.get(module_id) >= all_methods.length) {
                        this.logger.debug(`Reached max patches (${all_methods.length}) for module ${module_id}, unregistering`);
                        this.patch_targets.delete(module_id);
                    }

                    if (this.benchmark) {
                        const total_time = performance.now() - start_time;
                        this.logger.debug(`Successfully patched ${patched_count} method(s) in module ${module_id} (total: ${this.patch_counts.get(module_id)}) - ${total_time.toFixed(2)}ms`);
                    } else {
                        this.logger.debug(`Successfully patched ${patched_count} method(s) in module ${module_id} (total: ${this.patch_counts.get(module_id)})`);
                    }
                    return true;
                }

                return false;
            } catch (e) {
                if (this.benchmark) {
                    const total_time = performance.now() - start_time;
                    this.logger.error(`Error patching method in module ${module_id} after ${total_time.toFixed(2)}ms:`, e);
                } else {
                    this.logger.error(`Error patching method in module ${module_id}:`, e);
                }
                return false;
            }
        }

        _hook_webpack_array_push() {
            const original_push = this.global_obj.push;
            const self = this;

            this.global_obj.push = function(...args) {
                for (const module of args) {
                    if (Array.isArray(module) && module.length >= 2) {
                        const module_ids = module[0];

                        for (const module_id of module_ids) {
                            const patch_methods = self.patch_targets.get(module_id);
                            if (patch_methods && !self.patched_modules.has(module_id)) {
                                self.logger.debug(`Target module ${module_id} added, applying patch`);
                                self.patch_module(module, module_id, patch_methods);
                            }
                        }
                    }
                }
                return original_push.apply(this, args);
            }

            this.hooked = true;
            this.logger.debug("Webpack array push hooked");
        }

        _get_cached_function_string(func, module_id, method_name) {
            if (typeof func !== 'function') {
                return func;
            }

            const cache_key = `${module_id}_${method_name}`;
            if (this._function_string_cache.has(cache_key)) {
                return this._function_string_cache.get(cache_key);
            }

            const func_str = func.toString();
            this._function_string_cache.set(cache_key, func_str);
            return func_str;
        }
        _set_cached_function_string(func_str, module_id, method_name) {
            const cache_key = `${module_id}_${method_name}`;
            this._function_string_cache.set(cache_key, func_str);
        }

        /**
         * Method for cached method matching
         * @param {Function} method - The method to check
         * @param {string|RegExp} string_or_regex - The string or regex to match against
         * @param {number} module_id - Module in which the method is. Used for the key for caching
         * @param {string} method_name - Used for the key for caching
         * @returns {boolean} true if the method matches, false otherwise
         */
        does_method_match(method, string_or_regex, module_id, method_name) {
            const method_str = this._get_cached_function_string(method, module_id, method_name);
            if (typeof string_or_regex === 'string') {
                return method_str.includes(string_or_regex);
            } else if (string_or_regex instanceof RegExp) {
                return string_or_regex.test(method_str);
            }
            return false;
        }

        /**
         * Replace parts of a method's code using regexes or raw strings with caching
         * @param {Function} original_method - The method to patch, can also be a string of the method code
         * @param {Array} matches_and_replacements - Array of {match: RegExp|string, replacement: string|function, global: boolean} objects
         * @param {number} module_id - Module in which the method is. Used for the key for caching
         * @param {string} method_name - Used for the key for caching
         * @returns {Function} Patched method
         */
        patch_method_with_regexp_or_raw_string(original_method, matches_and_replacements, module_id, method_name) {
            const start_time = this.benchmark ? performance.now() : 0;
            let patched_code;
            try {
                const original_code = this._get_cached_function_string(original_method, module_id, method_name);
                patched_code = original_code;
                let total_replacements = 0;

                for (let i = 0; i < matches_and_replacements.length; i++) {
                    const match_and_replacement = matches_and_replacements[i];
                    const regex_start = this.benchmark ? performance.now() : 0;

                    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 method ${method_name} in module ${module_id}`);
                    }

                    if (this.benchmark) {
                        const regex_end = performance.now();
                        this.logger.debug(`Replacement ${i + 1}/${matches_and_replacements.length} ${replacement_occurred ? 'applied' : 'skipped (no match)'} in ${(regex_end - regex_start).toFixed(2)}ms`);
                    }
                }

                if (total_replacements === 0) {
                    this.logger.warn(`No replacements occured in method ${method_name} in module ${module_id}, returning original method`);
                    return original_method;
                }

                const patched_method = new Function("return " + patched_code)();
                this.logger.debug("Patched method:", patched_method);
                this._set_cached_function_string(patched_code, module_id, method_name);
                if (this.benchmark) {
                    const total_time = performance.now() - start_time;
                    this.logger.debug(`${total_replacements} replacement(s) applied (cached), method patched in ${total_time.toFixed(2)}ms`);
                }
                return patched_method;
            } catch (e) {
                if (this.benchmark) {
                    const total_time = performance.now() - start_time;
                    this.logger.error(`Replacement based patching failed after ${total_time.toFixed(2)}ms:`, e, "patched code:", patched_code);
                } else {
                    this.logger.error(`Replacement based patching failed:`, e, "patched code:", patched_code);
                }
                return original_method;
            }
        }

        /**
         * Static helper to reliably detect and hook webpack global object
         * @param {string} global_obj_key - Key of the global webpack object array (e.g. "webpackJsonpDeezer")
         * @param {function} callback - Callback to invoke once webpack is detected
         */
        static detect_and_hook_webpack(global_obj_key, callback) {
            if (window[global_obj_key]) {
                callback();
                return;
            }

            let webpack_found = false;
            Object.defineProperty(window, global_obj_key, {
                configurable: true,
                enumerable: true,
                set: function(value) {
                    if (!webpack_found) {
                        webpack_found = true;

                        Object.defineProperty(window, global_obj_key, {
                            configurable: true,
                            enumerable: true,
                            writable: true,
                            value: value
                        });

                        setTimeout(callback, 0);
                    }
                }
            });
        }

        /**
         * Initialize the global webpack patcher asynchronously
         */
        static async initialize(global_obj_key, logger, benchmark = false) {
            return new Promise((resolve) => {
                WebpackPatcher.detect_and_hook_webpack(global_obj_key, () => {
                    const patcher = new WebpackPatcher(global_obj_key, logger, benchmark);
                    logger.log("Global WebpackPatcher initialized");
                    resolve(patcher);
                });
            });
        }
    }


    /**
     * Helper class for registering patches with the global WebpackPatcher
     */
    class WebpackPatchRegistrar {
        constructor(patcher_promise) {
            this.patcher_promise = patcher_promise;
        }

        /**
         * Register multiple patches from a PATCHES object. Should be called as early as possible.
         *
         * @param {Object} patches - PATCHES object with the following structure:
         *
         * @structure
         * ```
         * PATCHES = {
         *   [module_id]: {
         *     methods: [
         *       {
         *         identifiers: [string | RegExp],    // Array of strings/regexes that all need to match
         *         matches_and_replacements: [        // Array of replacements to apply
         *           {
         *             match: string | RegExp,        // Pattern to match in method code
         *             replace: string | function,    // Replacement string or callback function
         *             global: boolean                // Whether to replace all occurrences (optional)
         *           }
         *         ]
         *       }
         *     ]
         *   }
         * }
         * ```
         *
         * Structure details:
         *   - **module_id**: Webpack module ID (number)
         *   - **methods**: Array of method patch configurations
         *   - **identifiers**: Array of strings or regexes that all need to match the target method.
         *                      **Identifiers should be as close to the start of the function as possible for performance**
         *   - **matches_and_replacements**: Array of text replacements to apply to the method
         *     - **match**: Pattern to find in method source code (string or RegExp)
         *     - **replace**: Replacement text (string) or callback function (match, ...groups) => string.
         *                   Functions should be used if you need access to capture groups or dynamic replacements.
         *     - **global**: If true, replaces all occurrences. For RegExp, uses regex.global flag if not specified
         */
        async register_patches(patches) {
            const patcher = await this.patcher_promise;
            patcher.register_patches(patches);
            logger.debug("Patches registered with WebpackPatcher");
            return patcher;
        }
    }


    class Logger {
        static PREFIX = "[WebpackPatcher]";

        constructor(debug=false) {
            this.should_debug = debug;
        }

        debug(...args) {if (this.should_debug) console.debug(Logger.PREFIX, ...args);}
        log(...args) {console.log(Logger.PREFIX, ...args);}
        warn(...args) {console.warn(Logger.PREFIX, ...args);}
        error(...args) {console.error(Logger.PREFIX, ...args);}
    }

    const logger = new Logger(false);

    const patcher_promise = WebpackPatcher.initialize("webpackJsonpDeezer", logger, false);
    const webpack_patch_registrar = new WebpackPatchRegistrar(patcher_promise);
    window.register_webpack_patches = webpack_patch_registrar.register_patches.bind(webpack_patch_registrar);
})();