From 758f318303f3f74ea70d795e1cbe564d7c3c4a88 Mon Sep 17 00:00:00 2001 From: NotNite Date: Tue, 25 Nov 2025 15:30:50 -0500 Subject: [PATCH 1/4] core: Support multiple Webpack scripts The primary motivation for this is loading libdiscoreWasmFetch along with web, as well as cleaning up Webpack patching code in general. We have to support *multiple requires* existing, so the main require in web gets marked with a special field. There's also multiple modules, some with conflicting IDs(!!!), so I'm not sure how that behaves. I do not know if patching modules in the other scripts works. I haven't tested this very well, so feedback is appreciated! Browser support isn't done yet, and noTrack will also need to be updated for the libdiscore launch signature now that it's actually being evaluated. --- packages/core/src/patch.ts | 177 +++++++++++++++----------- packages/node-preload/src/index.ts | 31 +++-- packages/types/src/discord/webpack.ts | 1 + 3 files changed, 125 insertions(+), 84 deletions(-) diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts index 6ec6f529..fb2fd6c3 100644 --- a/packages/core/src/patch.ts +++ b/packages/core/src/patch.ts @@ -285,7 +285,7 @@ function handleModuleDependencies() { } const injectedWpModules: IdentifiedWebpackModule[] = []; -function injectModules(entry: WebpackJsonpEntry[1]) { +function injectModules(entry: WebpackJsonpEntry[1], scanOnly?: boolean) { const modules: Record = {}; const entrypoints: string[] = []; let inject = false; @@ -321,61 +321,69 @@ function injectModules(entry: WebpackJsonpEntry[1]) { } } - webpackModules.delete(wpModule); - moonlight.pendingModules.delete(wpModule); - injectedWpModules.push(wpModule); - - inject = true; - - if (wpModule.run) { - modules[id] = wpModule.run; - wpModule.run.__moonlight = true; - // @ts-expect-error hacks - wpModule.run.call = (self, module, exports, require) => { - try { - wpModule.run!.apply(self, [module, exports, require]); - } catch (err) { - logger.error(`Failed to run module "${id}":`, err); - } - }; - if (wpModule.entrypoint) entrypoints.push(id); + if (!scanOnly) { + webpackModules.delete(wpModule); + moonlight.pendingModules.delete(wpModule); + injectedWpModules.push(wpModule); + + inject = true; + + if (wpModule.run) { + modules[id] = wpModule.run; + wpModule.run.__moonlight = true; + // @ts-expect-error hacks + wpModule.run.call = (self, module, exports, require) => { + try { + wpModule.run!.apply(self, [module, exports, require]); + } catch (err) { + logger.error(`Failed to run module "${id}":`, err); + } + }; + if (wpModule.entrypoint) entrypoints.push(id); + } } } + if (!webpackModules.size) break; } - for (const [name, func] of Object.entries(moonlight.moonmap.getWebpackModules("window.moonlight.moonmap"))) { - // @ts-expect-error probably should fix the type on this idk - func.__moonlight = true; - injectedWpModules.push({ id: name, run: func }); - modules[name] = func; - inject = true; - } + if (!scanOnly) { + for (const [name, func] of Object.entries(moonlight.moonmap.getWebpackModules("window.moonlight.moonmap"))) { + // @ts-expect-error probably should fix the type on this idk + func.__moonlight = true; + injectedWpModules.push({ id: name, run: func }); + modules[name] = func; + inject = true; + } - if (webpackRequire != null) { - for (const id of moonlight.moonmap.getLazyModules()) { - webpackRequire.e(id); + if (webpackRequire != null) { + for (const id of moonlight.moonmap.getLazyModules()) { + webpackRequire.e(id); + } } } if (inject) { - logger.debug("Injecting modules:", modules, entrypoints); + logger.trace("Injecting custom Webpack modules:", modules, entrypoints); window.webpackChunkdiscord_app.push([ [--chunkId], modules, - (require: WebpackRequireType) => - entrypoints.map((id) => { - try { - if (require.m[id] == null) { - logger.error(`Failing to load entrypoint module "${id}" because it's not found in Webpack.`); - } else { - return require(id); + (require: WebpackRequireType) => { + if (require.__moonlight) { + entrypoints.map((id) => { + try { + if (require.m[id] == null) { + logger.error(`Failing to load entrypoint module "${id}" because it's not found in Webpack.`); + } else { + require(id); + } + } catch (err) { + logger.error(`Failed to load entrypoint module "${id}":`, err); } - } catch (err) { - logger.error(`Failed to load entrypoint module "${id}":`, err); - } - return undefined; - }) + return undefined; + }); + } + } ]); } } @@ -414,47 +422,55 @@ export async function installWebpackPatcher() { run: wpRequireFetcher }); + // https://github.com/web-infra-dev/rspack/blob/2d78ee6c08d41ac538628c752fa97a8a75f4192f/crates/rspack_plugin_runtime/src/runtime_module/runtime/jsonp_chunk_loading_with_callback.ejs#L29-L31 let realWebpackJsonp: WebpackJsonp | null = null; Object.defineProperty(window, "webpackChunkdiscord_app", { set: (jsonp: WebpackJsonp) => { + // The load process does `chunk = chunk || []` so this doesn't really matter, but just in case + if (realWebpackJsonp) return; realWebpackJsonp = jsonp; - const realPush = jsonp.push; - if (jsonp.push.__moonlight !== true) { - jsonp.push = (items) => { - moonlight.events.dispatchEvent(WebEventType.ChunkLoad, { - chunkId: items[0], - modules: items[1], - require: items[2] - }); - patchModules(items[1]); + // .push is overriden for each Webpack entrypoint + let currentPush: WebpackJsonp["push"] = jsonp.push; + Object.defineProperty(jsonp, "push", { + set: (push: WebpackJsonp["push"]) => { + logger.trace("Overwriting Webpack push:", push); + currentPush = push; + }, + + get: () => { + const fakePush: WebpackJsonp["push"] = (items) => { + moonlight.events.dispatchEvent(WebEventType.ChunkLoad, { + chunkId: items[0], + modules: items[1], + require: items[2] + }); + + try { + patchModules(items[1]); + } catch (err) { + logger.warn("Failed to patch Webpack modules:", err); + } - try { - const res = realPush.apply(realWebpackJsonp, [items]); - if (!realPush.__moonlight) { - logger.trace("Injecting Webpack modules", items[1]); + try { + logger.trace("Injecting Webpack modules:", items[1]); + const res = currentPush.apply(realWebpackJsonp, [items]); injectModules(items[1]); + return res; + } catch (err) { + logger.error("Failed to inject Webpack modules:", err); + return 0; } + }; - return res; - } catch (err) { - logger.error("Failed to inject Webpack modules:", err); - return 0; - } - }; - - jsonp.push.bind = (thisArg: any, ...args: any[]) => { - return realPush.bind(thisArg, ...args); - }; - - jsonp.push.__moonlight = true; - if (!realPush.__moonlight) { - logger.debug("Injecting Webpack modules with empty entry"); - // Inject an empty entry to cause iteration to happen once - // Kind of a dirty hack but /shrug - injectModules({ deez: () => {} }); + // Multiple Webpack entrypoints load on top of each other by using `bind`, so we must pass through to the next push function so we don't cause an infinite loop + fakePush.bind = (thisArg: any, ...args: any[]) => { + return currentPush.bind(thisArg, ...args); + }; + + return fakePush; } - } + }); }, get: () => { @@ -462,18 +478,27 @@ export async function installWebpackPatcher() { } }); + // This is triggered when the individual Webpack entrypoints load with their initial modules Object.defineProperty(Function.prototype, "m", { configurable: true, set(modules: any) { const { stack } = new Error(); if (stack!.includes("/assets/") && !Array.isArray(modules)) { + // We should only use the "main" `require` when possible - this is how we distinguish them + if (stack!.includes("/assets/web.")) { + this.__moonlight = true; + } + + if (!window.webpackChunkdiscord_app) window.webpackChunkdiscord_app = []; moonlight.events.dispatchEvent(WebEventType.ChunkLoad, { modules: modules }); - patchModules(modules); - if (!window.webpackChunkdiscord_app) window.webpackChunkdiscord_app = []; - injectModules(modules); + if (this.__moonlight) { + patchModules(modules); + // We aren't early enough to actually inject modules, but we can start scanning for dependencies now + injectModules(modules, true); + } } Object.defineProperty(this, "m", { diff --git a/packages/node-preload/src/index.ts b/packages/node-preload/src/index.ts index a187937d..48afc5dd 100644 --- a/packages/node-preload/src/index.ts +++ b/packages/node-preload/src/index.ts @@ -162,15 +162,30 @@ if (isOverlay) { process.emit("loaded"); function replayScripts() { - const scripts = [...document.querySelectorAll("script")].filter( - (script) => script.src && blockedScripts.some((url) => url.includes(script.src)) - ); - - blockedScripts.reverse(); - for (const url of blockedScripts) { - if (!url.includes("/web.")) continue; + const ignoreScripts = [ + // We never blocked this in the first place + "popout.", + // We don't want this to load at all + "sentry." + ]; + + const scripts = [...document.querySelectorAll("script")].filter((script) => { + if (!script.src) return false; + + try { + const url = new URL(script.src); + const hasUrl = + url.pathname.match(/\/assets\/[a-zA-Z]+\./) && + !url.searchParams.has("inj") && + (url.host.endsWith("discord.com") || url.host.endsWith("discordapp.com")); + const shouldIgnore = ignoreScripts.some((other) => url.pathname.startsWith(`/assets/${other}`)); + return hasUrl && !shouldIgnore; + } catch { + return false; + } + }); - const script = scripts.find((script) => url.includes(script.src))!; + for (const script of scripts) { const newScript = document.createElement("script"); for (const attr of script.attributes) { if (attr.name === "src") attr.value += "?inj"; diff --git a/packages/types/src/discord/webpack.ts b/packages/types/src/discord/webpack.ts index 922e41d7..6d8bc8f7 100644 --- a/packages/types/src/discord/webpack.ts +++ b/packages/types/src/discord/webpack.ts @@ -6,6 +6,7 @@ export type WebpackRequireType = typeof MappingsWebpackRequire & c: Record; m: Record; e: (module: number | string) => Promise; + __moonlight?: boolean; }; export type WebpackModule = { From ae1e2f620ec8eee01801bfd0ba3702ada8183ce2 Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Sun, 15 Mar 2026 13:28:45 -0600 Subject: [PATCH 2/4] nodePreload: fix regex --- packages/node-preload/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-preload/src/index.ts b/packages/node-preload/src/index.ts index 48afc5dd..c6bc5749 100644 --- a/packages/node-preload/src/index.ts +++ b/packages/node-preload/src/index.ts @@ -175,7 +175,7 @@ if (isOverlay) { try { const url = new URL(script.src); const hasUrl = - url.pathname.match(/\/assets\/[a-zA-Z]+\./) && + url.pathname.match(/\/assets\/[a-zA-Z-]+\./) && !url.searchParams.has("inj") && (url.host.endsWith("discord.com") || url.host.endsWith("discordapp.com")); const shouldIgnore = ignoreScripts.some((other) => url.pathname.startsWith(`/assets/${other}`)); From 6ed871ddd832689ef45b49443142414117258db3 Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Fri, 20 Mar 2026 15:50:42 -0600 Subject: [PATCH 3/4] core/patch: Rework module injection a bit Most of this was to work around extensions adding markdown while having dependencies on React breaking with multi-webpack. Remapped stub modules now insert modules *before* injected modules are processed, meaning they should now inject on the same chunk as their remapped dependencies are added in some cases. Injected modules can now splice into chunks that are being currently inserted. This reduces chunk count (like it matters) and should be a slight speedup since a separate chunk doesn't need to be processed right after. The initial set of Webpack modules is now treated as a fake chunk and is also has injected modules spliced into it. Entrypoint modules are delayed slightly as they have to go into a new chunk to get required properly. (hopefully this is coherent enough rambling of how this works) --- packages/core/src/patch.ts | 135 +++++++++++++++----------- packages/types/src/discord/webpack.ts | 6 +- 2 files changed, 83 insertions(+), 58 deletions(-) diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts index fb2fd6c3..b73ca3cb 100644 --- a/packages/core/src/patch.ts +++ b/packages/core/src/patch.ts @@ -285,11 +285,25 @@ function handleModuleDependencies() { } const injectedWpModules: IdentifiedWebpackModule[] = []; -function injectModules(entry: WebpackJsonpEntry[1], scanOnly?: boolean) { +function injectModules(entry: WebpackJsonpEntry[1], splice?: boolean, fullEntry?: WebpackJsonpEntry) { const modules: Record = {}; const entrypoints: string[] = []; let inject = false; + for (const [name, func] of Object.entries(moonlight.moonmap.getWebpackModules("window.moonlight.moonmap"))) { + // @ts-expect-error probably should fix the type on this idk + func.__moonlight = true; + injectedWpModules.push({ id: name, run: func }); + modules[name] = func; + inject = true; + } + + if (webpackRequire != null) { + for (const id of moonlight.moonmap.getLazyModules()) { + webpackRequire.e(id); + } + } + for (const [_modId, mod] of Object.entries(entry)) { const modStr = mod.toString(); for (const wpModule of webpackModules) { @@ -321,70 +335,64 @@ function injectModules(entry: WebpackJsonpEntry[1], scanOnly?: boolean) { } } - if (!scanOnly) { - webpackModules.delete(wpModule); - moonlight.pendingModules.delete(wpModule); - injectedWpModules.push(wpModule); + webpackModules.delete(wpModule); + moonlight.pendingModules.delete(wpModule); + injectedWpModules.push(wpModule); - inject = true; + inject = true; - if (wpModule.run) { - modules[id] = wpModule.run; - wpModule.run.__moonlight = true; - // @ts-expect-error hacks - wpModule.run.call = (self, module, exports, require) => { - try { - wpModule.run!.apply(self, [module, exports, require]); - } catch (err) { - logger.error(`Failed to run module "${id}":`, err); - } - }; - if (wpModule.entrypoint) entrypoints.push(id); - } + if (wpModule.run) { + modules[id] = wpModule.run; + wpModule.run.__moonlight = true; + // @ts-expect-error hacks + wpModule.run.call = (self, module, exports, require) => { + try { + wpModule.run!.apply(self, [module, exports, require]); + } catch (err) { + logger.error(`Failed to run module "${id}":`, err); + } + }; + if (wpModule.entrypoint) entrypoints.push(id); } } if (!webpackModules.size) break; } - if (!scanOnly) { - for (const [name, func] of Object.entries(moonlight.moonmap.getWebpackModules("window.moonlight.moonmap"))) { - // @ts-expect-error probably should fix the type on this idk - func.__moonlight = true; - injectedWpModules.push({ id: name, run: func }); - modules[name] = func; - inject = true; - } - - if (webpackRequire != null) { - for (const id of moonlight.moonmap.getLazyModules()) { - webpackRequire.e(id); - } + if (inject) { + // biome-ignore lint/correctness/noUnusedFunctionParameters: keep ts happy + let oldEntry = (require: WebpackRequireType) => {}; + if (fullEntry?.[2] != null) { + oldEntry = fullEntry[2]; } - } - if (inject) { - logger.trace("Injecting custom Webpack modules:", modules, entrypoints); - window.webpackChunkdiscord_app.push([ - [--chunkId], - modules, - (require: WebpackRequireType) => { - if (require.__moonlight) { - entrypoints.map((id) => { - try { - if (require.m[id] == null) { - logger.error(`Failing to load entrypoint module "${id}" because it's not found in Webpack.`); - } else { - require(id); - } - } catch (err) { - logger.error(`Failed to load entrypoint module "${id}":`, err); + const loader = (require: WebpackRequireType) => { + if (require.__moonlight) { + entrypoints.map((id) => { + try { + if (require.m[id] == null) { + logger.error(`Failing to load entrypoint module "${id}" because it's not found in Webpack.`); + } else { + require(id); } - return undefined; - }); - } + } catch (err) { + logger.error(`Failed to load entrypoint module "${id}":`, err); + } + return undefined; + }); } - ]); + oldEntry(require); + }; + + if (splice && fullEntry != null) { + logger.trace("Splicing custom Webpack modules:", fullEntry[0], modules, entrypoints, entry); + fullEntry[1] = { ...fullEntry[1], ...modules }; + + if (entrypoints.length > 0) fullEntry[2] = loader; + } else { + logger.trace("Injecting custom Webpack modules:", modules, entrypoints); + window.webpackChunkdiscord_app.push([[--chunkId], modules, entrypoints.length > 0 ? loader : undefined]); + } } } @@ -453,9 +461,9 @@ export async function installWebpackPatcher() { } try { + injectModules(items[1], true, items); logger.trace("Injecting Webpack modules:", items[1]); const res = currentPush.apply(realWebpackJsonp, [items]); - injectModules(items[1]); return res; } catch (err) { logger.error("Failed to inject Webpack modules:", err); @@ -481,7 +489,7 @@ export async function installWebpackPatcher() { // This is triggered when the individual Webpack entrypoints load with their initial modules Object.defineProperty(Function.prototype, "m", { configurable: true, - set(modules: any) { + set(modules: { [id: string]: WebpackModuleFunc }) { const { stack } = new Error(); if (stack!.includes("/assets/") && !Array.isArray(modules)) { // We should only use the "main" `require` when possible - this is how we distinguish them @@ -496,8 +504,21 @@ export async function installWebpackPatcher() { if (this.__moonlight) { patchModules(modules); - // We aren't early enough to actually inject modules, but we can start scanning for dependencies now - injectModules(modules, true); + const fakeEntry: WebpackJsonpEntry = [[chunkId], { ...modules }, undefined]; + injectModules(modules, true, fakeEntry); + + const newModules = fakeEntry[1]; + const modKeys = Object.keys(modules); + const newModKeys = Object.keys(newModules); + const newMods = newModKeys.filter((key) => !modKeys.includes(key)); + + if (newMods.length > 0) { + logger.trace("Splicing into initial Webpack modules!", newMods); + for (const id of newMods) { + modules[id] = newModules[id]; + } + if (fakeEntry[2] != null) window.webpackChunkdiscord_app.push([[chunkId--], {}, fakeEntry[2]]); + } } } diff --git a/packages/types/src/discord/webpack.ts b/packages/types/src/discord/webpack.ts index 6d8bc8f7..96037172 100644 --- a/packages/types/src/discord/webpack.ts +++ b/packages/types/src/discord/webpack.ts @@ -19,7 +19,11 @@ export type WebpackModuleFunc = ((module: any, exports: any, require: WebpackReq __moonlight?: boolean; }; -export type WebpackJsonpEntry = [number[], { [id: string]: WebpackModuleFunc }, (require: WebpackRequireType) => any]; +export type WebpackJsonpEntry = [ + number[], + { [id: string]: WebpackModuleFunc }, + ((require: WebpackRequireType) => any) | undefined +]; export type WebpackJsonp = WebpackJsonpEntry[] & { push: { From 98d57936826a9eb1c5f5096803e9cbaf8ffe620f Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Fri, 20 Mar 2026 22:09:23 -0600 Subject: [PATCH 4/4] core/patch: Stopgap `launch_signature` fix I genuinely do not know any other better way to do this without forcing extensions to load later (which means either forcing authors to rework things or outright breaking what I fixed in an earlier commit; neither are desireable). This is still early enough in the load cycle that no HTTP requests are made yet, so none of them are missing `launch_signature` in their super properties. --- packages/core/src/patch.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts index b73ca3cb..65e6ca87 100644 --- a/packages/core/src/patch.ts +++ b/packages/core/src/patch.ts @@ -430,6 +430,30 @@ export async function installWebpackPatcher() { run: wpRequireFetcher }); + registerPatch({ + find: ".extendSuperProperties({launch_signature:", + replace: [ + { + match: /=performance\.now\(\),(\i)=(.+?:null);(\i)\.extendSuperProperties\({launch_signature:\i}\);/, + replacement: (_, sigVar, launch_signature, PropertiesHelper) => `=performance.now(),${sigVar}=null; + let _libdiscoreInitialized=false; + Object.defineProperty(window,"_libdiscoreInitialized",{ + set: (x)=>{ + _libdiscoreInitialized = x; + const sig = ${launch_signature}; + ${sigVar} = sig; + ${PropertiesHelper}.extendSuperProperties({ + launch_signature: sig + }); + }, + get: () => _libdiscoreInitialized + });` + } + ], + ext: "analyticsHelperFix", + id: 0 + }); + // https://github.com/web-infra-dev/rspack/blob/2d78ee6c08d41ac538628c752fa97a8a75f4192f/crates/rspack_plugin_runtime/src/runtime_module/runtime/jsonp_chunk_loading_with_callback.ejs#L29-L31 let realWebpackJsonp: WebpackJsonp | null = null; Object.defineProperty(window, "webpackChunkdiscord_app", {