diff --git a/.vscode/settings.json b/.vscode/settings.json index ce5aaa2..eeb77dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ - "replugged" + "replugged", + "unpatch" ] } diff --git a/src/api/ModImplementation.ts b/src/api/ModImplementation.ts index 5ffae34..99c732e 100644 --- a/src/api/ModImplementation.ts +++ b/src/api/ModImplementation.ts @@ -1,7 +1,9 @@ import { WebpackApi } from "./Webpack.js"; +import { PatcherApi } from "./Patcher.js"; export interface IModImplementation { WebpackApi: typeof WebpackApi, + PatcherApi: typeof PatcherApi.prototype, /** * shall be true when a mod requires the Dev to bundle their code into single file */ diff --git a/src/api/Patcher.ts b/src/api/Patcher.ts new file mode 100644 index 0000000..5793bb1 --- /dev/null +++ b/src/api/Patcher.ts @@ -0,0 +1,26 @@ +export interface IBasePatcherApi { + internalId: string | undefined; /* NOT ON REPLUGGED, on BD it's the "caller" for Patcher functions */ + unpatchAll(): void; + after(target: T, name: string, cb: (args: A, res: R, instance: T) => R): () => void; +} + +class DummyPatcherApi implements IBasePatcherApi { + internalId: string | undefined; + unpatchAll(): void { + throw new Error("Method not implemented. This is a dummy class."); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + after(target: T, name: string, cb: (args: A, res: R, instance: T) => R): () => void { + throw new Error("Method not implemented. This is a dummy class."); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + before(target: T, name: string, cb: (args: A, instance: T) => A): () => void { + throw new Error("Method not implemented. This is a dummy class."); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + instead(target: T, name: string, cb: (args: A, orig: ((...args_: A) => R), instance: T) => R): () => void { + throw new Error("Method not implemented. This is a dummy class."); + } +} + +export const PatcherApi = DummyPatcherApi; diff --git a/src/api/RuntimeGenerators.ts b/src/api/RuntimeGenerators.ts index 82a08ae..43fa8da 100644 --- a/src/api/RuntimeGenerators.ts +++ b/src/api/RuntimeGenerators.ts @@ -32,6 +32,13 @@ export function createFunctionWithWrapperNeeded(objectName: string, property: st Object.defineProperty(result, "wrapperName", { value: wrapperName }); return result; } +export function createFunctionThatIsMissing() { + const result = new Function(`return () => {throw new Error("Missing");}`)(); + Object.defineProperty(result, "missing", { + value: true, + }); + return result; +} import { FunctionImplementation, __requireInternal, doesImplement, implementationStores, initStores } from "../common/index.js"; import { createJavaScriptFromObject, getKeyValue } from "../utils.js"; @@ -120,7 +127,7 @@ export async function addCode(mod: IModImplementation) { } } // const rawCode = "globalThis.implementationStores = {\n" + getMain(serializer).serialize(constructed) + "\n}"; - const req = (target: any) => new Function("return {" + target.func + "}.func;")(); + const req = (target: any) => new Function("return {" + target.func + "}.func" + (target.asImmediatelyInvokedFunctionExpression === "true" ? "();" : ";"))(); const rawCode = `${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_VAR_NAME} = (${createJavaScriptFromObject(constructed, true)}); ${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_REQ} = ${req.toString()};`; diff --git a/src/common/PatcherApi.ts b/src/common/PatcherApi.ts new file mode 100644 index 0000000..1340f39 --- /dev/null +++ b/src/common/PatcherApi.ts @@ -0,0 +1,76 @@ +import { FunctionImplementation, __requireInternal } from "./index.js"; +import { IModImplementation } from "../api/ModImplementation.js"; +import { IBasePatcherApi } from "../api/Patcher.js"; +export let targetMod: IModImplementation; + +const implementationStore = { + Patcher_constructor: new FunctionImplementation({ + data: null, + depends: [], + supplies: "constructor_", + isWrapper: true, + asImmediatelyInvokedFunctionExpression: true, + func() { + return { + internalId: Date.now().toString(), + get after() { + return __requireInternal(targetMod, "PatcherApi", "after")?.bind(undefined, this); + }, + get unpatchAll() { + return __requireInternal(targetMod, "PatcherApi", "unpatchAll")?.bind(undefined, this); + }, + get before() { + return __requireInternal(targetMod, "PatcherApi", "after")?.bind(undefined, this); + }, + get instead() { + return __requireInternal(targetMod, "PatcherApi", "instead")?.bind(undefined, this); + }, + }; + }, + }), + afterWrapper: new FunctionImplementation({ + data: null, + depends: [], + supplies: "after", + isWrapper: true, + func(thisObj: IBasePatcherApi, target: T, name: string, cb: (args: A, res: R, instance: T) => R): () => void { + return __requireInternal(targetMod, "PatcherApi", "after", true)!(thisObj.internalId, target, name, (instance_: T, args_: A, res_: R) => { + return cb(args_, res_, instance_); + }); + }, + }), + unpatchAllWrapper: new FunctionImplementation({ + data: null, + depends: [], + supplies: "unpatchAll", + isWrapper: true, + func(thisObj: IBasePatcherApi) { + return __requireInternal(targetMod, "PatcherApi", "unpatchAll", true)!(thisObj.internalId); + }, + }), + beforeWrapper: new FunctionImplementation({ + data: null, + depends: [], + supplies: "before", + isWrapper: true, + func(thisObj: IBasePatcherApi, target: T, name: string, cb: (args: A, instance: T) => A): () => void { // in replugged callback needs to return arguments. what happens in BD? + return __requireInternal(targetMod, "PatcherApi", "before", true)!(thisObj.internalId, target, name, (instance_: T, args_: A) => { + return cb(args_, instance_); + }); + }, + }), + insteadWrapper: new FunctionImplementation({ + data: null, + depends: [], + supplies: "instead", + isWrapper: true, + func(thisObj: IBasePatcherApi, target: T, name: string, cb: (args: A, orig: ((...args_: A) => R), instance: T) => A): () => void { + return __requireInternal(targetMod, "PatcherApi", "instead", true)!(thisObj.internalId, target, name, (instance_: T, args_: A, orig: ((...args__: A) => R)) => { + return cb(args_, orig, instance_); + }); + }, + }), +} as { [key: string]: FunctionImplementation }; +export { + implementationStore, +}; diff --git a/src/common/WebpackApi.ts b/src/common/WebpackApi.ts index 69ce0f0..e4a0500 100644 --- a/src/common/WebpackApi.ts +++ b/src/common/WebpackApi.ts @@ -49,7 +49,6 @@ const implementationStore = { supplies: "getByStrings", data: null, func(...strings) { - /* __requireInternal(targetMod, "WebpackApi", "test")!(); */ const getModule = __requireInternal(targetMod, "WebpackApi", "getModule"); if (!getModule) throw new Error("Unimplemented"); @@ -66,16 +65,6 @@ const implementationStore = { }); }, }), - test: new FunctionImplementation({ - data: null, - depends: ["getByStrings"], - supplies: "test", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - func(...args: any[]) { - debugger; - return "the test worked"; - }, - }), } as { [key: string]: FunctionImplementation }; export { implementationStore, diff --git a/src/common/index.ts b/src/common/index.ts index ad97aa3..ac9a00a 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -7,6 +7,7 @@ export interface IFunctionImplementation { data: any, func: (...args: any[]) => any, isWrapper?: boolean, + asImmediatelyInvokedFunctionExpression?: boolean; } class FunctionImplementation implements IFunctionImplementation { supplies: string; @@ -14,6 +15,7 @@ class FunctionImplementation implements IFunctionImplementation { data: any; func: (...args: any[]) => any; isWrapper?: boolean | undefined; + asImmediatelyInvokedFunctionExpression?: boolean | undefined; // constructor(supplies: string, depends: string[], data: any, func: (...args: any[]) => any) { // this.supplies = supplies; // this.depends = depends; @@ -21,13 +23,14 @@ class FunctionImplementation implements IFunctionImplementation { // this.func = func; // } constructor(options: IFunctionImplementation) { - const { supplies, depends, data, func, isWrapper } = options; + const { supplies, depends, data, func, isWrapper, asImmediatelyInvokedFunctionExpression } = options; this.supplies = supplies!; this.depends = depends!; Object.defineProperty(this, "data", { value: data, enumerable: data !== null }); this.func = func!; // this.isWrapper = isWrapper === true; Object.defineProperty(this, "isWrapper", { value: isWrapper, enumerable: isWrapper === true }); + Object.defineProperty(this, "asImmediatelyInvokedFunctionExpression", { value: asImmediatelyInvokedFunctionExpression, enumerable: asImmediatelyInvokedFunctionExpression === true }); } } export { @@ -56,7 +59,11 @@ export async function initStores() { } export function doesImplement(mod: IModImplementation, category: string, method: string) { const categoryObj = getKeyValue(mod, category as keyof IModImplementation); - return getKeyValue(categoryObj, method as never) != undefined; + const value = getKeyValue(categoryObj, method as never); + if (value === undefined || (value as ({ missing: boolean })).missing === true) { + return false; + } + return true; } export function __requireInternal(mod: IModImplementation, category: string, method: string, ignoreWrappers: boolean = false) { diff --git a/src/converter.ts b/src/converter.ts index 4d8b1a2..af89185 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { ParseResult } from "@babel/parser"; -import { File, Identifier, ImportDeclaration, ImportSpecifier, MemberExpression, Statement, callExpression, identifier, memberExpression, stringLiteral } from "@babel/types"; +import { File, Identifier, ImportDeclaration, ImportSpecifier, MemberExpression, NewExpression, Standardized, Statement, callExpression, identifier, memberExpression, newExpression, stringLiteral } from "@babel/types"; import { NonFunctionType, getKeyValue, myPackageName } from "./utils.js"; import { IModImplementation } from "./api/ModImplementation"; import { addCode } from "./api/RuntimeGenerators.js"; import { IMPLEMENTATION_STORES_PATH_REQ, IMPLEMENTATION_STORES_PATH_SOURCE, IMPLEMENTATION_STORES_PATH_VAR_NAME } from "./constants.js"; -function removeASTLocation(ast: Statement[] | Statement) { +function removeASTLocation(ast: Standardized[] | Standardized) { if (Array.isArray(ast)) { ast.forEach(a => removeASTLocation(a)); } @@ -97,7 +97,7 @@ function deepFind(obj: any, path: string): K | undefined { export default async function (ast: ParseResult, targetedDiscordModApiLibrary: { default: IModImplementation }): Promise { const parsedBody = ast.program.body; - const importStatements = parsedBody.filter(x => x.type == "ImportDeclaration") as Statement[]; + const importStatements = parsedBody.filter(x => x.type == "ImportDeclaration") as ImportDeclaration[]; const importAliasMap = [] as { internalName: string, codeName: string }[]; removeASTLocation(importStatements); const importsToRemove: number[] = []; @@ -106,7 +106,6 @@ export default async function (ast: ParseResult, targetedDiscordModApiLibr // spec.local.name = "test_"; // alias // // @ts-ignore // spec.imported.name = "test"; // imported value - // debugger; if (element.source.value == myPackageName) { // checking if it's the same module as we are for (let index2 = 0; index2 < element.specifiers.length; index2++) { const spec = element.specifiers[index2] as ImportSpecifier; @@ -120,7 +119,7 @@ export default async function (ast: ParseResult, targetedDiscordModApiLibr } const trueImportsToRemove = importStatements.filter((_, index) => importsToRemove.includes(index)); - const parsedBodyWithoutOurImports = parsedBody.filter((item, index) => !trueImportsToRemove.includes(parsedBody[index])); + const parsedBodyWithoutOurImports = parsedBody.filter((_, index) => !trueImportsToRemove.includes(parsedBody[index] as ImportDeclaration)); // parsedBodyWithoutOurImports.unshift(...await addCode(targetedDiscordModApiLibrary.default)); for (let index = 0; index < parsedBodyWithoutOurImports.length; index++) { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -129,15 +128,55 @@ export default async function (ast: ParseResult, targetedDiscordModApiLibr * Example: * WebpackApi.getModule(something) -> "WebpackApi" matches signature of an element from importsToBake array -> import WebpackApi ourselves -> find method getModule -> select replacement based on target client mod -> read target's object path and property name -> replace them */ - debugger; // console.log(findAllTypesWithPath(element, "MemberExpression")); - const paths = findPathsToType({ obj: element, targetType: "MemberExpression" }); - for (let index2 = 0; index2 < paths.length; index2++) { - const element2 = paths[index2]; - const trueObj = deepFind(element, element2); + const newExpressionPaths = findPathsToType({ obj: element, targetType: "NewExpression" }); + for (const newExpressionPath of newExpressionPaths) { + const trueObj = deepFind(element, newExpressionPath); + console.log(trueObj); + if (trueObj != undefined && importAliasMap.find(x => x.codeName == (trueObj.callee as Identifier).name) !== undefined) { + removeASTLocation(trueObj); + const importedInternalName = importAliasMap.find(x => x.codeName == (trueObj.callee as Identifier).name)!.internalName; + const propDesc = Object.getOwnPropertyDescriptor(targetedDiscordModApiLibrary.default, importedInternalName as keyof IModImplementation); + if (!propDesc) + continue; + const result: IModImplementation[keyof IModImplementation] = propDesc.value ?? propDesc.get!(); + if (result == undefined || typeof result === "boolean") + continue; + const { constructor } = result; + // @ts-expect-error This for sure is a constructor + const constructed = new constructor(); + if (constructed.constructor_?.wrapperName) { + const originalObj = importedInternalName; + for (const prop of Object.getOwnPropertyNames(trueObj)) { + // @ts-expect-error well + delete trueObj[prop]; + } + const newCallExpr = callExpression(memberExpression(identifier(IMPLEMENTATION_STORES_PATH_SOURCE), identifier(IMPLEMENTATION_STORES_PATH_REQ)), [ + memberExpression(memberExpression(memberExpression(identifier(IMPLEMENTATION_STORES_PATH_SOURCE), identifier(IMPLEMENTATION_STORES_PATH_VAR_NAME)), identifier(originalObj)), identifier(constructed.constructor_.wrapperName)), + ]); + Object.assign(trueObj, newCallExpr); + continue; + } + const newAst = newExpression( + memberExpression( + identifier(constructed.object), + identifier(constructed.property), + ), + [], + ); + for (const prop of Object.getOwnPropertyNames(trueObj)) { + // @ts-expect-error well + delete trueObj[prop]; + } + Object.assign(trueObj, newAst); + } + } + const memberExpressionPaths = findPathsToType({ obj: element, targetType: "MemberExpression" }); + for (const memberExpressionPath of memberExpressionPaths) { + const trueObj = deepFind(element, memberExpressionPath); console.log(trueObj); if (trueObj != undefined && importAliasMap.find(x => x.codeName == (trueObj.object as Identifier).name) !== undefined) { - removeASTLocation(trueObj as unknown as Statement); + removeASTLocation(trueObj); const importedInternalName = importAliasMap.find(x => x.codeName == (trueObj.object as Identifier).name)!.internalName; const propDesc = Object.getOwnPropertyDescriptor(targetedDiscordModApiLibrary.default, importedInternalName as keyof IModImplementation); if (!propDesc) diff --git a/src/converters/betterdiscord.ts b/src/converters/betterdiscord.ts index 041bb04..e96d544 100644 --- a/src/converters/betterdiscord.ts +++ b/src/converters/betterdiscord.ts @@ -1,7 +1,8 @@ import { Statement } from "@babel/types"; import { IModImplementation } from "../api/ModImplementation.js"; -import { createFunctionFromObjectProperty } from "../api/RuntimeGenerators.js"; +import { createFunctionFromObjectProperty, createFunctionWithWrapperNeeded } from "../api/RuntimeGenerators.js"; import { IBaseWebpackApi } from "../api/Webpack.js"; +import { IBasePatcherApi } from "../api/Patcher.js"; class BDWebpackApi implements IBaseWebpackApi { get getModule() { @@ -9,11 +10,31 @@ class BDWebpackApi implements IBaseWebpackApi { } } +class BDPatcherApi implements IBasePatcherApi { + get constructor_() { + return createFunctionWithWrapperNeeded("undefined", "undefined", "Patcher_constructor") as any; + } + internalId!: string; + get unpatchAll() { + return createFunctionWithWrapperNeeded("BdApi.Patcher", "unpatchAll", "unpatchAllWrapper"); + } + get after() { + return createFunctionWithWrapperNeeded("BdApi.Patcher", "after", "afterWrapper"); + } + get before() { + return createFunctionWithWrapperNeeded("BdApi.Patcher", "before", "beforeWrapper"); + } + get instead() { + return createFunctionWithWrapperNeeded("BdApi.Patcher", "instead", "insteadWrapper"); + } +} + export function convertFormat(ast: Statement[]) { return ast; } export default { WebpackApi: new BDWebpackApi(), + PatcherApi: BDPatcherApi.prototype, importsForbidden: true, } as IModImplementation; diff --git a/src/converters/replugged.ts b/src/converters/replugged.ts index c99d9fa..0778bf3 100644 --- a/src/converters/replugged.ts +++ b/src/converters/replugged.ts @@ -1,8 +1,9 @@ import { ClassDeclaration, ClassMethod, ExportDefaultDeclaration, Identifier, Statement } from "@babel/types"; import { IModImplementation } from "../api/ModImplementation.js"; -import { createFunctionWithWrapperNeeded } from "../api/RuntimeGenerators.js"; +import { createFunctionFromObjectProperty, createFunctionWithWrapperNeeded } from "../api/RuntimeGenerators.js"; import { IBaseWebpackApi } from "../api/Webpack.js"; import { parse } from "@babel/parser"; +import { IBasePatcherApi } from "../api/Patcher.js"; class RPWebpackApi implements IBaseWebpackApi { get getModule() { @@ -10,6 +11,28 @@ class RPWebpackApi implements IBaseWebpackApi { } } +class RPPatcherApi implements IBasePatcherApi { + get constructor_() { + return createFunctionFromObjectProperty("replugged", "Injector") as any; + } + constructor() { + return this.constructor_; + } + internalId: undefined; + get unpatchAll() { + return createFunctionFromObjectProperty("replugged.Injector.constructor", "unpatchAllWrapper"); + } + get after() { + return createFunctionFromObjectProperty("replugged.Injector.constructor", "afterWrapper"); + } + get before() { + return createFunctionFromObjectProperty("replugged.Injector.constructor", "beforeWrapper"); + } + get instead() { + return createFunctionFromObjectProperty("replugged.Injector.constructor", "insteadWrapper"); + } +} + export function convertFormat(ast: Statement[]) { let targetClassName = undefined; for (const astNode of ast) { @@ -42,4 +65,5 @@ export function convertFormat(ast: Statement[]) { export default { WebpackApi: new RPWebpackApi(), + PatcherApi: RPPatcherApi.prototype, } as IModImplementation; diff --git a/src/index.ts b/src/index.ts index 3c7b07e..2475222 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ -export * from './api/Webpack.js'; \ No newline at end of file +export * from './api/Webpack.js'; +export * from './api/Patcher.js'; diff --git a/test/sample/index.js b/test/sample/index.js index 008b030..2e885a7 100644 --- a/test/sample/index.js +++ b/test/sample/index.js @@ -1,4 +1,4 @@ -import { WebpackApi } from "discord-mod-compiler"; +import { WebpackApi, PatcherApi } from "discord-mod-compiler"; export default class CrossCompiledSample { start() { @@ -23,6 +23,8 @@ export default class CrossCompiledSample { } else { console.log("WebpackApi.getModule is working"); } + const test = new PatcherApi(); + test.before(someModuleType3, "sendMessage", console.log); } stop() { }