this repo has no description

Merge pull request #58 from moonlight-mod/adryd/expose-internals

Expose patcher internals

adryd a8c4767e c78791c4

Changed files
+212 -61
packages
core
src
extension
util
types
web-preload
+5 -4
packages/core/src/extension/loader.ts
···
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");
···
extensions,
dependencyGraph
}: ProcessedExtensions) {
-
const eventEmitter = createEventEmitter();
+
const eventEmitter = createEventEmitter<EventType, EventPayloads>();
const finished: Set<string> = new Set();
logger.trace(
···
}
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();
})
);
···
await loadExt(ext);
finished.add(ext.id);
-
eventEmitter.dispatchEvent("ext-ready", ext.id);
+
eventEmitter.dispatchEvent(EventType.ExtensionLoad, ext.id);
logger.debug(`Loaded "${ext.id}"`);
}
+57 -4
packages/core/src/patch.ts
···
import Logger from "./util/logger";
import calculateDependencies, { Dependency } from "./util/dependency";
import WebpackRequire from "@moonlight-mod/types/discord/require";
+
import { EventType } from "@moonlight-mod/types/core/event";
const logger = new Logger("core/patch");
···
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);
···
webpackModules.add(wp);
if (wp.dependencies?.length) {
moonlight.pendingModules.add(wp);
+
}
+
}
+
+
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]);
+
}
}
}
···
}
}
+
// 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;
}
}
···
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}`);
}
···
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);
···
} 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);
···
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 {
···
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);
+77 -45
packages/core/src/util/event.ts
···
-
export type MoonlightEventCallback = (data: string) => void;
-
-
export interface MoonlightEventEmitter {
-
dispatchEvent: (id: string, data: string) => void;
-
addEventListener: (id: string, cb: MoonlightEventCallback) => void;
-
removeEventListener: (id: string, cb: MoonlightEventCallback) => void;
-
}
+
import { MoonlightEventEmitter } from "@moonlight-mod/types/core/event";
-
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>();
+
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: string, data: string) => {
-
eventEmitter.dispatchEvent(new CustomEvent(id, { detail: data }));
-
},
+
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;
-
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 as EventData[Id]);
+
}
-
function listener(e: Event) {
-
const event = e as CustomEvent<string>;
-
cb(event.detail);
-
}
+
listeners.set(untyped, listener);
+
eventEmitter.addEventListener(id as string, listener);
+
},
-
listeners.set(cb, listener);
-
eventEmitter.addEventListener(id, 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);
+
}
+
};
+
}
-
removeEventListener: (id: string, cb: (data: string) => void) => {
-
const listener = listeners.get(cb);
-
if (listener == null) return;
-
listeners.delete(cb);
-
eventEmitter.removeEventListener(id, listener);
-
}
-
};
+
export function createEventEmitter<
+
EventId extends string = string,
+
EventData = Record<EventId, any>
+
>(): MoonlightEventEmitter<EventId, EventData> {
+
webPreload: {
+
return webMethod();
}
nodePreload: {
+33
packages/types/src/core/event.ts
···
+
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;
+
};
+1 -1
packages/types/src/extension.ts
···
};
export type ExplicitExtensionDependency = {
-
ext: string;
+
ext?: string;
id: string;
};
+10
packages/types/src/globals.ts
···
ProcessedExtensions
} from "./extension";
import EventEmitter from "events";
+
import { EventPayloads, EventType, MoonlightEventEmitter } from "./core/event";
export type MoonlightHost = {
asarPath: string;
···
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;
+2 -1
packages/web-preload/package.json
···
"name": "@moonlight-mod/web-preload",
"private": true,
"dependencies": {
-
"@moonlight-mod/core": "workspace:*"
+
"@moonlight-mod/core": "workspace:*",
+
"@moonlight-mod/types": "workspace:*"
}
}
+14 -1
packages/web-preload/src/index.ts
···
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 "@moonlight-mod/core/util/event";
+
import { EventPayloads, EventType } from "@moonlight-mod/types/core/event";
(async () => {
const logger = new Logger("web-preload");
···
unpatched: new Set(),
pendingModules: new Set(),
enabledExtensions: new Set(),
+
events: createEventEmitter<EventType, EventPayloads>(),
+
patchingInternals: {
+
onModuleLoad,
+
registerPatch,
+
registerWebpackModule
+
},
getConfig: moonlightNode.getConfig.bind(moonlightNode),
getConfigOption: moonlightNode.getConfigOption.bind(moonlightNode),
+13 -5
pnpm-lock.yaml
···
devDependencies:
'@typescript-eslint/eslint-plugin':
specifier: ^6.13.2
-
version: 6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.2)
+
version: 6.13.2(@typescript-eslint/parser@6.13.2(eslint@8.55.0)(typescript@5.3.2))(eslint@8.55.0)(typescript@5.3.2)
'@typescript-eslint/parser':
specifier: ^6.13.2
version: 6.13.2(eslint@8.55.0)(typescript@5.3.2)
···
version: 9.1.0(eslint@8.55.0)
eslint-plugin-prettier:
specifier: ^5.0.1
-
version: 5.0.1(eslint-config-prettier@9.1.0)(eslint@8.55.0)(prettier@3.1.0)
+
version: 5.0.1(eslint-config-prettier@9.1.0(eslint@8.55.0))(eslint@8.55.0)(prettier@3.1.0)
eslint-plugin-react:
specifier: ^7.33.2
version: 7.33.2(eslint@8.55.0)
···
'@moonlight-mod/core':
specifier: workspace:*
version: link:../core
+
'@moonlight-mod/types':
+
specifier: workspace:*
+
version: link:../types
packages:
···
'@types/semver@7.5.6': {}
-
'@typescript-eslint/eslint-plugin@6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.2)':
+
'@typescript-eslint/eslint-plugin@6.13.2(@typescript-eslint/parser@6.13.2(eslint@8.55.0)(typescript@5.3.2))(eslint@8.55.0)(typescript@5.3.2)':
dependencies:
'@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 6.13.2(eslint@8.55.0)(typescript@5.3.2)
···
natural-compare: 1.4.0
semver: 7.5.4
ts-api-utils: 1.0.3(typescript@5.3.2)
+
optionalDependencies:
typescript: 5.3.2
transitivePeerDependencies:
- supports-color
···
'@typescript-eslint/visitor-keys': 6.13.2
debug: 4.3.4
eslint: 8.55.0
+
optionalDependencies:
typescript: 5.3.2
transitivePeerDependencies:
- supports-color
···
debug: 4.3.4
eslint: 8.55.0
ts-api-utils: 1.0.3(typescript@5.3.2)
+
optionalDependencies:
typescript: 5.3.2
transitivePeerDependencies:
- supports-color
···
is-glob: 4.0.3
semver: 7.5.4
ts-api-utils: 1.0.3(typescript@5.3.2)
+
optionalDependencies:
typescript: 5.3.2
transitivePeerDependencies:
- supports-color
···
dependencies:
eslint: 8.55.0
-
eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.1.0)(eslint@8.55.0)(prettier@3.1.0):
+
eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.1.0(eslint@8.55.0))(eslint@8.55.0)(prettier@3.1.0):
dependencies:
eslint: 8.55.0
-
eslint-config-prettier: 9.1.0(eslint@8.55.0)
prettier: 3.1.0
prettier-linter-helpers: 1.0.0
synckit: 0.8.6
+
optionalDependencies:
+
eslint-config-prettier: 9.1.0(eslint@8.55.0)
eslint-plugin-react@7.33.2(eslint@8.55.0):
dependencies: