Skip to content

Commit

Permalink
core/patcher: export registerPatch and RegisterWebpackModule
Browse files Browse the repository at this point in the history
core/patcher: add moduleLoad callback
core/util/event: refactor with types, thanks @NotNite
web-preload: add patcherInternals to window.moonlight
  • Loading branch information
adryd325 committed Oct 1, 2024
1 parent c78791c commit 4905e4d
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 59 deletions.
9 changes: 5 additions & 4 deletions packages/core/src/extension/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -159,7 +160,7 @@ export async function loadProcessedExtensions({
extensions,
dependencyGraph
}: ProcessedExtensions) {
const eventEmitter = createEventEmitter();
const eventEmitter = createEventEmitter<EventType, EventPayloads>();
const finished: Set<string> = new Set();

logger.trace(
Expand All @@ -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();
})
);
Expand All @@ -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}"`);
}

Expand Down
61 changes: 57 additions & 4 deletions packages/core/src/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ 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");

// Can't be Set because we need splice
const patches: IdentifiedPatch[] = [];
let webpackModules: Set<IdentifiedWebpackModule> = new Set();

const moduleLoadSubscriptions: Map<string, ((moduleId: string) => void)[]> =
new Map();

export function registerPatch(patch: IdentifiedPatch) {
patches.push(patch);
moonlight.unpatched.add(patch);
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -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);

Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
130 changes: 81 additions & 49 deletions packages/core/src/util/event.ts
Original file line number Diff line number Diff line change
@@ -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<EventId, any>
>(): MoonlightEventEmitter<EventId, EventData> {
const EventEmitter = require("events");
const eventEmitter = new EventEmitter();
const listeners = new Map<MoonlightEventCallback, (...args: any[]) => void>();
const listeners = new Map<(data: EventData) => void, (e: Event) => void>();

return {
dispatchEvent: (id: string, data: string) => {
eventEmitter.emit(id, data);
dispatchEvent: <Id extends keyof EventData>(
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 extends keyof EventData>(
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<string>;
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 extends keyof EventData>(
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<MoonlightEventCallback, (e: Event) => 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<string>;
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<EventId, any>
>(): MoonlightEventEmitter<EventId, EventData> {
const eventEmitter = new EventTarget();
const listeners = new Map<(data: EventData) => void, (e: Event) => void>();

return {
dispatchEvent: <Id extends keyof EventData>(
id: Id,
data: EventData[Id]
) => {
eventEmitter.dispatchEvent(
new CustomEvent(id as string, { detail: data })
);
},

addEventListener: <Id extends keyof EventData>(
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<string>;
cb(event.detail as EventData[Id]);
}
};

listeners.set(untyped, listener);
eventEmitter.addEventListener(id as string, listener);
},

removeEventListener: <Id extends keyof EventData>(
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<EventId, any>
>(): MoonlightEventEmitter<EventId, EventData> {
webPreload: {
return webMethod();
}

nodePreload: {
Expand Down
33 changes: 33 additions & 0 deletions packages/types/src/core/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { WebpackModuleFunc, WebpackRequireType } from "../discord";

export interface MoonlightEventEmitter<
EventId extends string = string,
EventData = Record<EventId, any>
> {
dispatchEvent: <Id extends keyof EventData>(
id: Id,
data: EventData[Id]
) => void;
addEventListener: <Id extends keyof EventData>(
id: Id,
cb: (data: EventData[Id]) => void
) => void;
removeEventListener: <Id extends keyof EventData>(
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;
};
2 changes: 1 addition & 1 deletion packages/types/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export type Patch = {
};

export type ExplicitExtensionDependency = {
ext: string;
ext?: string;
id: string;
};

Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ProcessedExtensions
} from "./extension";
import EventEmitter from "events";
import { EventPayloads, EventType, MoonlightEventEmitter } from "./core/event";

export type MoonlightHost = {
asarPath: string;
Expand Down Expand Up @@ -39,6 +40,15 @@ export type MoonlightWeb = {
unpatched: Set<IdentifiedPatch>;
pendingModules: Set<IdentifiedWebpackModule>;
enabledExtensions: Set<string>;
events: MoonlightEventEmitter<EventType, EventPayloads>;
patchingInternals: {
onModuleLoad: (
moduleId: string | string[],
callback: (moduleId: string) => void
) => void;
registerPatch: (patch: IdentifiedPatch) => void;
registerWebpackModule: (module: IdentifiedWebpackModule) => void;
};

getConfig: (ext: string) => ConfigExtension["config"];
getConfigOption: <T>(ext: string, name: string) => T | undefined;
Expand Down
Loading

0 comments on commit 4905e4d

Please sign in to comment.