From 4905e4dcc8cbc5db652716a776f9d67e31136326 Mon Sep 17 00:00:00 2001 From: adryd Date: Mon, 30 Sep 2024 23:19:08 -0400 Subject: [PATCH] core/patcher: export registerPatch and RegisterWebpackModule core/patcher: add moduleLoad callback core/util/event: refactor with types, thanks @notnite web-preload: add patcherInternals to window.moonlight --- packages/core/src/extension/loader.ts | 9 +- packages/core/src/patch.ts | 61 +++++++++++- packages/core/src/util/event.ts | 130 ++++++++++++++++---------- packages/types/src/core/event.ts | 33 +++++++ packages/types/src/extension.ts | 2 +- packages/types/src/globals.ts | 10 ++ packages/web-preload/src/index.ts | 15 ++- 7 files changed, 201 insertions(+), 59 deletions(-) create mode 100644 packages/types/src/core/event.ts diff --git a/packages/core/src/extension/loader.ts b/packages/core/src/extension/loader.ts index b7d696a..a88d645 100644 --- a/packages/core/src/extension/loader.ts +++ b/packages/core/src/extension/loader.ts @@ -10,6 +10,7 @@ import { registerPatch, registerWebpackModule } from "../patch"; import calculateDependencies from "../util/dependency"; import { createEventEmitter } from "../util/event"; import { registerStyles } from "../styles"; +import { EventPayloads, EventType } from "@moonlight-mod/types/core/event"; const logger = new Logger("core/extension/loader"); @@ -159,7 +160,7 @@ export async function loadProcessedExtensions({ extensions, dependencyGraph }: ProcessedExtensions) { - const eventEmitter = createEventEmitter(); + const eventEmitter = createEventEmitter(); const finished: Set = new Set(); logger.trace( @@ -181,11 +182,11 @@ export async function loadProcessedExtensions({ } function done() { - eventEmitter.removeEventListener("ext-ready", cb); + eventEmitter.removeEventListener(EventType.ExtensionLoad, cb); r(); } - eventEmitter.addEventListener("ext-ready", cb); + eventEmitter.addEventListener(EventType.ExtensionLoad, cb); if (finished.has(dep)) done(); }) ); @@ -201,7 +202,7 @@ export async function loadProcessedExtensions({ await loadExt(ext); finished.add(ext.id); - eventEmitter.dispatchEvent("ext-ready", ext.id); + eventEmitter.dispatchEvent(EventType.ExtensionLoad, ext.id); logger.debug(`Loaded "${ext.id}"`); } diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts index 3664fb1..36956a4 100644 --- a/packages/core/src/patch.ts +++ b/packages/core/src/patch.ts @@ -11,6 +11,7 @@ import { import Logger from "./util/logger"; import calculateDependencies, { Dependency } from "./util/dependency"; import WebpackRequire from "@moonlight-mod/types/discord/require"; +import { EventType } from "types/src/core/event"; const logger = new Logger("core/patch"); @@ -18,6 +19,9 @@ const logger = new Logger("core/patch"); const patches: IdentifiedPatch[] = []; let webpackModules: Set = new Set(); +const moduleLoadSubscriptions: Map void)[]> = + new Map(); + export function registerPatch(patch: IdentifiedPatch) { patches.push(patch); moonlight.unpatched.add(patch); @@ -30,6 +34,25 @@ export function registerWebpackModule(wp: IdentifiedWebpackModule) { } } +export function onModuleLoad( + module: string | string[], + callback: (moduleId: string) => void +): void { + let moduleIds = module; + + if (typeof module === "string") { + moduleIds = [module]; + } + + for (const moduleId of moduleIds) { + if (moduleLoadSubscriptions.has(moduleId)) { + moduleLoadSubscriptions.get(moduleId)?.push(callback); + } else { + moduleLoadSubscriptions.set(moduleId, [callback]); + } + } +} + /* The patching system functions by matching a string or regex against the .toString()'d copy of a Webpack module. When a patch happens, we reconstruct @@ -161,6 +184,19 @@ function patchModules(entry: WebpackJsonpEntry[1]) { } } + // Dispatch module load event subscription + if (moduleLoadSubscriptions.has(id)) { + const loadCallbacks = moduleLoadSubscriptions.get(id)!; + for (const callback of loadCallbacks) { + try { + callback(id); + } catch (e) { + logger.error("Error in module load subscription: " + e); + } + } + moduleLoadSubscriptions.delete(id); + } + moduleCache[id] = moduleString; } } @@ -192,7 +228,12 @@ function handleModuleDependencies() { const deps = item.data?.dependencies ?? []; return ( deps.filter( - (dep) => !(dep instanceof RegExp || typeof dep === "string") + (dep) => + !( + dep instanceof RegExp || + typeof dep === "string" || + dep.ext === undefined + ) ) as ExplicitExtensionDependency[] ).map((x) => `${x.ext}_${x.id}`); } @@ -210,7 +251,7 @@ function injectModules(entry: WebpackJsonpEntry[1]) { for (const [_modId, mod] of Object.entries(entry)) { const modStr = mod.toString(); for (const wpModule of webpackModules) { - const id = wpModule.ext + "_" + wpModule.id; + const id = wpModule.ext ? wpModule.ext + "_" + wpModule.id : wpModule.id; if (wpModule.dependencies) { const deps = new Set(wpModule.dependencies); @@ -223,8 +264,10 @@ function injectModules(entry: WebpackJsonpEntry[1]) { } else if (dep instanceof RegExp) { if (dep.test(modStr)) deps.delete(dep); } else if ( - injectedWpModules.find( - (x) => x.ext === dep.ext && x.id === dep.id + injectedWpModules.find((x) => + wpModule.ext + ? x.ext === dep.ext && x.id === dep.id + : x.id === dep.id ) ) { deps.delete(dep); @@ -293,6 +336,12 @@ export async function installWebpackPatcher() { const realPush = jsonp.push; if (jsonp.push.__moonlight !== true) { jsonp.push = (items) => { + moonlight.events.dispatchEvent(EventType.ChunkLoad, { + chunkId: items[0], + modules: items[1], + require: items[2] + }); + patchModules(items[1]); try { @@ -335,7 +384,11 @@ export async function installWebpackPatcher() { set(modules: any) { const { stack } = new Error(); if (stack!.includes("/assets/") && !Array.isArray(modules)) { + moonlight.events.dispatchEvent(EventType.ChunkLoad, { + modules: modules + }); patchModules(modules); + if (!window.webpackChunkdiscord_app) window.webpackChunkdiscord_app = []; injectModules(modules); diff --git a/packages/core/src/util/event.ts b/packages/core/src/util/event.ts index dd606e1..42fa1cb 100644 --- a/packages/core/src/util/event.ts +++ b/packages/core/src/util/event.ts @@ -1,70 +1,102 @@ -export type MoonlightEventCallback = (data: string) => void; +import { MoonlightEventEmitter } from "@moonlight-mod/types/core/event"; -export interface MoonlightEventEmitter { - dispatchEvent: (id: string, data: string) => void; - addEventListener: (id: string, cb: MoonlightEventCallback) => void; - removeEventListener: (id: string, cb: MoonlightEventCallback) => void; -} - -function nodeMethod(): MoonlightEventEmitter { +function nodeMethod< + EventId extends string = string, + EventData = Record +>(): MoonlightEventEmitter { const EventEmitter = require("events"); const eventEmitter = new EventEmitter(); - const listeners = new Map void>(); + const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); return { - dispatchEvent: (id: string, data: string) => { - eventEmitter.emit(id, data); + dispatchEvent: ( + id: Id, + data: EventData[Id] + ) => { + eventEmitter.emit(id as string, data); }, - addEventListener: (id: string, cb: (data: string) => void) => { - if (listeners.has(cb)) return; + addEventListener: ( + id: Id, + cb: (data: EventData[Id]) => void + ) => { + const untyped = cb as (data: EventData) => void; + if (listeners.has(untyped)) return; - function listener(data: string) { - cb(data); + function listener(e: Event) { + const event = e as CustomEvent; + cb(event as EventData[Id]); } - listeners.set(cb, listener); - eventEmitter.on(id, listener); + listeners.set(untyped, listener); + eventEmitter.on(id as string, listener); }, - removeEventListener: (id: string, cb: (data: string) => void) => { - const listener = listeners.get(cb); + removeEventListener: ( + id: Id, + cb: (data: EventData[Id]) => void + ) => { + const untyped = cb as (data: EventData) => void; + const listener = listeners.get(untyped); if (listener == null) return; - listeners.delete(cb); - eventEmitter.off(id, listener); + listeners.delete(untyped); + eventEmitter.off(id as string, listener); } }; } -export function createEventEmitter(): MoonlightEventEmitter { - webPreload: { - const eventEmitter = new EventTarget(); - const listeners = new Map void>(); - - return { - dispatchEvent: (id: string, data: string) => { - eventEmitter.dispatchEvent(new CustomEvent(id, { detail: data })); - }, - - addEventListener: (id: string, cb: (data: string) => void) => { - if (listeners.has(cb)) return; - - function listener(e: Event) { - const event = e as CustomEvent; - cb(event.detail); - } - - listeners.set(cb, listener); - eventEmitter.addEventListener(id, listener); - }, - - removeEventListener: (id: string, cb: (data: string) => void) => { - const listener = listeners.get(cb); - if (listener == null) return; - listeners.delete(cb); - eventEmitter.removeEventListener(id, listener); +function webMethod< + EventId extends string = string, + EventData = Record +>(): MoonlightEventEmitter { + const eventEmitter = new EventTarget(); + const listeners = new Map<(data: EventData) => void, (e: Event) => void>(); + + return { + dispatchEvent: ( + id: Id, + data: EventData[Id] + ) => { + eventEmitter.dispatchEvent( + new CustomEvent(id as string, { detail: data }) + ); + }, + + addEventListener: ( + id: Id, + cb: (data: EventData[Id]) => void + ) => { + const untyped = cb as (data: EventData) => void; + if (listeners.has(untyped)) return; + + function listener(e: Event) { + const event = e as CustomEvent; + cb(event.detail as EventData[Id]); } - }; + + listeners.set(untyped, listener); + eventEmitter.addEventListener(id as string, listener); + }, + + removeEventListener: ( + id: Id, + cb: (data: EventData[Id]) => void + ) => { + const untyped = cb as (data: EventData) => void; + const listener = listeners.get(untyped); + if (listener == null) return; + listeners.delete(untyped); + eventEmitter.removeEventListener(id as string, listener); + } + }; +} + +export function createEventEmitter< + EventId extends string = string, + EventData = Record +>(): MoonlightEventEmitter { + webPreload: { + return webMethod(); } nodePreload: { diff --git a/packages/types/src/core/event.ts b/packages/types/src/core/event.ts new file mode 100644 index 0000000..e9dc342 --- /dev/null +++ b/packages/types/src/core/event.ts @@ -0,0 +1,33 @@ +import { WebpackModuleFunc, WebpackRequireType } from "../discord"; + +export interface MoonlightEventEmitter< + EventId extends string = string, + EventData = Record +> { + dispatchEvent: ( + id: Id, + data: EventData[Id] + ) => void; + addEventListener: ( + id: Id, + cb: (data: EventData[Id]) => void + ) => void; + removeEventListener: ( + id: Id, + cb: (data: EventData[Id]) => void + ) => void; +} + +export enum EventType { + ChunkLoad = "chunkLoad", + ExtensionLoad = "extensionLoad" +} + +export type EventPayloads = { + [EventType.ChunkLoad]: { + chunkId?: number[]; + modules: { [id: string]: WebpackModuleFunc }; + require?: (require: WebpackRequireType) => any; + }; + [EventType.ExtensionLoad]: string; +}; diff --git a/packages/types/src/extension.ts b/packages/types/src/extension.ts index bad087a..b35856a 100644 --- a/packages/types/src/extension.ts +++ b/packages/types/src/extension.ts @@ -100,7 +100,7 @@ export type Patch = { }; export type ExplicitExtensionDependency = { - ext: string; + ext?: string; id: string; }; diff --git a/packages/types/src/globals.ts b/packages/types/src/globals.ts index f31324c..fa08ecf 100644 --- a/packages/types/src/globals.ts +++ b/packages/types/src/globals.ts @@ -7,6 +7,7 @@ import { ProcessedExtensions } from "./extension"; import EventEmitter from "events"; +import { EventPayloads, EventType, MoonlightEventEmitter } from "./core/event"; export type MoonlightHost = { asarPath: string; @@ -39,6 +40,15 @@ export type MoonlightWeb = { unpatched: Set; pendingModules: Set; enabledExtensions: Set; + events: MoonlightEventEmitter; + patchingInternals: { + onModuleLoad: ( + moduleId: string | string[], + callback: (moduleId: string) => void + ) => void; + registerPatch: (patch: IdentifiedPatch) => void; + registerWebpackModule: (module: IdentifiedWebpackModule) => void; + }; getConfig: (ext: string) => ConfigExtension["config"]; getConfigOption: (ext: string, name: string) => T | undefined; diff --git a/packages/web-preload/src/index.ts b/packages/web-preload/src/index.ts index eaebeaa..4774d24 100644 --- a/packages/web-preload/src/index.ts +++ b/packages/web-preload/src/index.ts @@ -1,7 +1,14 @@ import { loadProcessedExtensions } from "@moonlight-mod/core/extension/loader"; -import { installWebpackPatcher } from "@moonlight-mod/core/patch"; +import { + installWebpackPatcher, + onModuleLoad, + registerPatch, + registerWebpackModule +} from "@moonlight-mod/core/patch"; import { installStyles } from "@moonlight-mod/core/styles"; import Logger from "@moonlight-mod/core/util/logger"; +import { createEventEmitter } from "core/src/util/event"; +import { EventPayloads, EventType } from "types/src/core/event"; (async () => { const logger = new Logger("web-preload"); @@ -10,6 +17,12 @@ import Logger from "@moonlight-mod/core/util/logger"; unpatched: new Set(), pendingModules: new Set(), enabledExtensions: new Set(), + events: createEventEmitter(), + patchingInternals: { + onModuleLoad, + registerPatch, + registerWebpackModule + }, getConfig: moonlightNode.getConfig.bind(moonlightNode), getConfigOption: moonlightNode.getConfigOption.bind(moonlightNode),