Skip to content

Instantly share code, notes, and snippets.

@Vendicated
Last active May 21, 2024 20:14
Show Gist options
  • Save Vendicated/dec16d3fd86f761558ce170d016ebc53 to your computer and use it in GitHub Desktop.
Save Vendicated/dec16d3fd86f761558ce170d016ebc53 to your computer and use it in GitHub Desktop.

Discord Webpack / Modding Guide

This guide will give you a general idea of

  • How to interact with Discord's modules (aka how webpack works and how to find things)
  • How to actually find the right functions for your job

Instead of just telling you how to do things without you understanding why it works this way, I will instead try to make you understand how things work and then how to utilise this knowledge!

Note: While being about Discord, this guide more or less applies to any app using Electron/webpack so it might even be helpful for other apps!

Prerequisites

You should be familiar with:

  • Javascript (especially the Browser side of things), HTML and CSS
  • React
  • Chrome's DevTools

In case you aren't familiar with some of these, I highly recommend you try them out in a normal app first. For example, if you never had any experience with Web Development, you could make a personal homepage with React.

While you can of course learn these things while modding, you will work with minified Code with all documentation stripped out, so making a normal project will be way easier for someone new to this!

Now that you're ready to get started, install React Developer Tools. The process of doing this will differ per mod and environment, in Vencord it's as easy as enabling them in the settings, if you're using something else, figure it out yourself (or ask me :D)!

Webpack's require (Interacting with Discord's modules)

When making a Javascript app, you obviously don't want all your code in one single file. This is where Node's require comes into play, an API allowing you to import different files. File 1 specifies what it wants to export:

module.exports = {
    hello() {
        console.log("Hello was called!")
    }
}

and now file 2 can import this function:

require("./file1").hello();

But require only exists in Node, the Browser doesn't know require!

In comes webpack, a tool that bundles a codebase using require to a web consumable build! Webpack also has a require function, webpackRequire. Except instead of filenames, it uses automatically generated ids for each module: require("./foo") -> require(17271). What a bummer!

Here's an example from a random Discord module: webpack require code sample

But as you probably already realised, we have no way of knowing what id a module has, so we can't just use webpack's require. We have to find a different method!

The require cache

Imagine a module like

let count = 0;
function increment() {
    count++
}

module.exports = {
    count,
    increment
}

If you imported this module from multiple files, you would expect all files to use the same counter. And that's exactly why require caches all imported modules. The first time you require a module, it's loaded and initialised and cached, then if you require it again you get the cached version.

You can actually view this cache via require.cache! In our case above it would look something like this:

{
    foo: {
        loaded: true,
        id: "foo",
        exports: {
            count: 0,
            increment() {
                count++
            }
        }
    }
}

Now if you loop over this cache, you can access the exports of arbitrary modules without knowing their id as long as they have been imported before. You can probably already guess where this is going?

Finding Modules in the require cache

Note: The following code examples are only very basic to explain the basics and don't cover all cases. For a better implementation, check out the Vencord webpack source code!

We can now find the counter module without knowing its name:

function findCounter() {
    // Iterate over the cache
    for (const id in require.cache) {
        // Check if the current module has an increment export
        if (require.cache[id].exports.increment) {
            // yippee! We found the counter module
            return require.cache[id].exports;
        }
    }
    // nothing found >:(
    return null;
}

const Counter = findCounter();
Counter.increment();

But we obviously don't want to only find the counter, we want to find way more modules.

findByProps

So let's make the method generic!

function findModuleByProperty(property: string) {
    for (const id in require.cache) {
        // Check if the current module has the specified property
        if (property in require.cache[id].exports) {
            // yippee! We found a matching module
            return require.cache[id].exports;
        }
    }
    // nothing found >:(
    return null;
}

const Counter = findModuleByProperty("increment");
Counter.increment();

findByCode

Sadly it's not always that simple! A lot of Discord modules don't have friendly exports, so it's more like

export.s = function() {
    return "read if cute"
}

findModuleByProperties("s") is not really viable... 😕 There will be many modules with that same export name and it will change every update

But surely Discord will only have one function returning "read if cute" so we can find the function by its code!!

function findModuleByCode(code: string) {
    for (const id in require.cache) {
        const currentModule = require.cache[id];
        // since the export names are also unusable, we need a second inner loop to test every export
        for (const name in currentModule) {
            // Check only the current export, in our case `exports.s`
            const export = currentModule[name];
            // Since we're searching a function by code, we first need to make sure this is actually a function!
            if (typeof export === "function") {
                // Thankfully, we're in Javascript which allows you to get the code of
                // any function as string!
                const functionCode = export.toString();
                if (functionCode.includes(code)) {
                    // woah! This function includes the code we searched for, let's return it
                    return export;
                    // Note that now we only returned the single function we found, because
                    // otherwise we'd be in the same dilemma of not knowing the name again!
                    // If you wanted the full module, you could also return both
                    // the module and the name we found, so the user has more control!
                    return [currentModule, name];
                }
            }
        }
    }
    // nothing found >:(
    return null;
}

const getMessage = findModuleByCode("read if cute");
console.log(getMessage()) // read if cute

// or if you use the alternative return
const [module, getMessageName] = findModuleByCode("read if cute");
console.log(module[getMessageName]()) // read if cute

find (with custom filter function)

Usually, findByProps or findByCode will be all you need! However, in some rarer cases, you will need a more specific filter, for example a filter that finds a module whose USER export is a function.

function findModuleByFilter(filter: (currentModule: any) => boolean) {
    for (const id in require.cache) {
        const currentModule = require.cache[id];
        // Call the filter with the current module
        if (filter(currentModule)) {
            // The filter function matched this module!
            return currentModule;
        }
    }
    // nothing found >:(
    return null;
}

const ModuleWithUserFunction = findModuleByFilter(m => typeof m.USER === "function")
ModuleWithUserFunction.USER()

Webpack find wrap-up

I hope these examples helped you understand how finding modules works. It's nothing more than checking every single module in the cache until one matches your search.

Depending on your use case, you'll use one of those three methods of finding modules. Also different mods may have more find methods, like findByRegex, findByName, and so on, but they're all basically the same just with different methods of finding things!

Quick Summary:

  • Use findByProps to find a module by export name, e.g. findByProps("foo") to find exports.foo
  • Use findByCode to find a single function by its code signature
  • Use find to find a module with a custom filter if the other two won't suffice

Btw: You don't actually have to worry about the implementation of the find methods, you only really need to know how to use them. I just showed you basic examples so you understand how it works! Feel free to forget about them again :P

Now you know how to find modules once you know what you're looking for. But...

How do I even find the function that does what I do?

This is where Chrome and React DevTools come into play.

Chrome DevTools allow you to search Discord's code, put breakpoints and run code

React Devtools allow you to to inspect UI elements like Buttons and see all their internals, like the callback for when you click on it

I usually use the following approaches:

  • Is there a UI element that already does what I want? For example, if I wanted to make an Emote Cloner plugin, to figure out how to create emotes, my first instinct would be to use React Devtools to inspect the Create Emote button in the Server Settings. Now inspect the onClick and similar and see what happens. Don't hesitate to put breakpoints and follow along each step!
  • If React Devtools weren't of help, use the regular document inspector to find the html classes of the desired element. Now choose the class that seems the most specific, for example avatar-2e8lTP. Remove the junk part and search the code (see below) for ().CLASS, like ().avatar. Now you will find all components that use this class (or another class with the same name)!
  • Search Discord's code for keywords related to your feature. In the same example as above, I would search for "createEmoji", "createCustomEmoji", "createEmote", "uploadCustomEmoji" ... until I find something promising. You can search all Discord files at once by pressing ctrl shift f while having DevTools open. Make sure to enable pretty print btw! (DevTools Settings -> Experimental -> Always pretty print). Check all results until you find something that looks right, then put a breakpoint and do whatever would trigger this method to be called. Now you can look at all the arguments passed to it and so on

Found the right method?

I found the function! Now how do I find the module

For this part, it's important that you understand how a module looks.

Essentially, webpack creates a bunch of chunks (multiple big bundles), where each chunk contains multiple modules. Think of it like chunk = folder, module = file in folder

Each module will have a similar syntax:

MODULE_ID: (module, exports, require) => {
    addExports(exports, {
        exportOne: () => theExportedThing,
        exportTwo: () => theExportedThing
    });

    var import1 = require(MODULE_1_ID),
        import2 = require(MODULE_2_ID)

    // code
}

A real world example:

So once you found the right function, the first step would be to check the exports of the module its in to see if it's even exported. Scroll up until you see the familiar pattern of

12172: (e, t, n) => {

}

Check the exports to see which locals are exported and if the one you want is part of it.

You can also now require this module by id using webpackRequire to get its exports in your console.

In Vencord this would be Vencord.Webpack.wreq(12172) (wreq stands for webpack require). If you're using a different mod, this might be different or not possible (if so, bug its dev to expose it, it's really useful!)

As you can see in the screenshot above, Discord's export names are sadly all short and unusable. But here's some advice on the names:

  • export.Z = export.default with no more exports
  • export.ZP = export.default with more named exports

Now how you would write the filter depends on what you're trying to find:

  • Object: findByProps
  • single function: findByCode
  • class: find(m => m.prototype?.someInstanceMethod)

Since you already required the module by id, you now have the exports in your console. Experiment with the above approaches until you find a working filter.

IMPORTANT: Once you found a working filter, make sure this filter is actually unique! This means, feed the filter into findAll to find all matches. If there's more than one match, you have to make your filter more specific until only exactly the right module matches!

Common Pitfalls

  • Make sure your FILTERS are UNIQUE! You don't want it to randomly find a different module all of a sudden
  • When searching by code, NEVER RELY ON MINIFIED VARIABLE NAMES. They will change in the future and break your plugin
  • Webpack search is EXTREMELY EXPENSIVE. Only do it once, and never do it inside React components, this will cause horrible lag. Also consider making Webpack search lazy (only done on demand). Vencord for example provides the lazyWebpack utility
  • React was not covered in this document, however, if you can, wrap your React UI into an ErrorBoundary to avoid nasty crashes. On Vencord for example, ErrorBoundary.wrap(MyComponent) or <ErrorBoundary><MyComponent /></ErrorBoundary>
  • Some modules like context menus are lazily loaded (only loaded once the user opens a context menu), so you will have to get those in a different way. Vencord for example provides the waitFor utility

The more nerdy side of things

Webpack in Detail

Webpack creates a global on the Window which is an Array with overwritten push function. In Discord' case, this is webpackChunkdiscord_app.

Every time a new chunk is loaded, it pushes itself into this Array in the following form:

webpackChunkdiscord_app.push([
    [CHUNK_ID],
    {
        MODULE_1_ID: (module, exports, require) => {
            exports.foo = "12"
        }
    },
    optionalCallback: require => {}
])

Once this chunk has been pushed, you can now require its MODULE_1_ID module via require(MODULE_1_ID)

Yes, you can register your own modules in Discord's webpack with this! Funky, huh?

Getting webpack's require

const webpackRequire = webpackChunkdiscord_app.push([
    [Symbol()], // Make sure our "chunk" id is unique
    {} // No modules,
    require => require // the return value of this function will be the return value of push
]);

Notable members on require

const requireCache = webpackRequire.c;
requireCache[1234].exports.foo

const registeredModules = webpackRequire.m; // Includes all registered modules, even those that haven't been loaded

const requireMain = webpackRequire.s; // The entry point
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment