Skip to content

bertigert/Webpack-Patcher

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 

Repository files navigation

Webpack Patcher

This script is a runtime patching helper script for applications that use Webpack (e.g., Electron renderer processes or browser contexts).
It allows you to intercept and modify webpack module code.

It works by hooking into webpack’s internal module system (__webpack_require__), intercepting the moment modules are defined and modifying their factory functions before they execute.


Overview

Since this script patches modules before they are loaded/executed, it is necessary to initialize this script as early as possible. That's why this is a standalone script and not a library or similar.

This also means that other scripts which utilize this script need to load and register patches as soon as possible. See example scripts for a way to do that.

Global interface

After initialization, the following is exposed on window.WebpackPatcher:

Property Type Description
register(options, patches) Function Registers patches. Returns the registrar object.
addEventListener(event, callback) Function Adds a listener for webpack_detected, module_registered, or module_patched.
removeEventListener(event, callback) Function Removes an event listener.
webpackRequire Function | null The detected webpack require function.
moduleFactories Object | null Map of all registered module factories.
moduleCache Object Wrapper for interacting with the webpack cache.
patches Array All patch configurations.
patchedModules Set IDs of modules that can be patched.
isWebpackDetected boolean True once webpack has been detected and hooked.
Registrars Object Registry of all user-defined patches.
placeholders Object Special placeholder values replaced in patched code. Used in replacements
VERSION number|String Current patcher version.

Registrar system

The main entry point for a script is the WebpackPatcher.register(options, patches) function.

  • object options - A simple centralized place for your script to store data:
    • object options.data - Place to store variables etc.
    • object options.functions - Place to store functions.

While it is not necessary for you to use the structure provided by the script, it's probably a good thing to use a standardized structure.

  • array patches - The array of patches

This is the structure of a registration:

WebpackPatcher.register_patches(
    { // options
        name: string, // required, creates/gets WebpackPatcher.Registrars[name]
        data: object, // initial data object for the registrar
        functions: object // initial functions object for the registrar
    },
    [ // patches
        {
            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
                }
            ]
        }
    ]
);

Using placeholders

WebpackPatcher provides special placeholder tokens you can use inside replacements. They are available at window.WebpackPatcher.placeholders and are replaced with concrete references to your registrar before the patched module code is evaluated.

  • window.WebpackPatcher.placeholders.self — replaced with window.WebpackPatcher.Registrars["<your-registrar-name>"]
  • window.WebpackPatcher.placeholders.functions — replaced with window.WebpackPatcher.Registrars["<your-registrar-name>"].functions
  • window.WebpackPatcher.placeholders.data — replaced with window.WebpackPatcher.Registrars["<your-registrar-name>"].data

Example: use a placeholder in a replacement so the patched module calls a helper from your registrar:

{
    match: "SOME_GLOBAL",
    replace: window.WebpackPatcher.placeholders.functions + ".myHelper(arg)"
}

Important: do not hard-code the internal token strings (they include a random suffix). Always read the values from window.WebpackPatcher.placeholders so the correct tokens are used at runtime.

Usage in other scripts

An easy way to use this script is the following:

(function wait_for_webpack_patcher(){
    if (window.WebpackPatcher) {
        logger.debug("Registering webpack patches");
        window.WebpackPatcher.register({
            name: name
        }, PATCHES);
    } else if (!window.GLOBAL_WEBPACK_ARRAY) { // e.g. webpackChunkdiscord_app for discord.com
        setTimeout(wait_for_webpack_patcher, 0);
    } else {
        logger.warn("Webpack array found, but not patcher, stopping");
    }
})();

We wait for WebpackPatcher to be available, but stop if the global webpack array is found with WebpackPatcher still being unavailable. That's because if that case is true, then something broke in the Webpack Patcher.

Modifying the script for other sites

The script is meant to be very easily ported to other sites. The only thing which should need changing is the CONFIGURATIONS array.

CONFIGURATIONS array

The CONFIGURATIONS constant is an ordered array of configuration objects that let the patcher pick site-specific options automatically. The first configuration for which site_match() is true gets picked, so avoid ambigious filters.

Each entry has the shape:

  • site_match function (required) - a function which returns true if this is a site we want to patch
  • options object - options passed to the patcher when the site matches. See the initialize function for more details.

Structure

{
    site_match: () => location.hostname === "example.com",
    options: {
        filter_func: () => {}
    }
}
/**
 * @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"})
*/
function initialize(logger, options={}) {}

The most important part is the filter_func. This should filter out the main webpack instance, the best way is to check for the caller's file. It should be the file which initializes the main part of webpack. Often has "web" or "runtime" in it's name.

Examples

See the examples for different sites in examples.

Officially supported sites

Links

GitHub

Greazyfork

Credits

Vencord Web - Inspiration on the hooking method

About

Library script to patch the code of webpack modules at runtime.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published