diff --git a/src/api/RuntimeGenerators.ts b/src/api/RuntimeGenerators.ts index f7ecb4d..82a08ae 100644 --- a/src/api/RuntimeGenerators.ts +++ b/src/api/RuntimeGenerators.ts @@ -18,7 +18,7 @@ export function createFunctionFromPath(path: string) { * @returns A function that returns the value of the specified property from the object. */ export function createFunctionFromObjectProperty(objectName: string, property: string) { - const generatedFunction: () => any = new Function(`return () => ${objectName}.${property}`)(); + const generatedFunction: (...args: any[]) => any = new Function(`return () => ${objectName}.${property}`)(); Object.defineProperty(generatedFunction, "object", { value: objectName, }); @@ -27,3 +27,104 @@ export function createFunctionFromObjectProperty(objectName: string, property: s }); return generatedFunction; } +export function createFunctionWithWrapperNeeded(objectName: string, property: string, wrapperName: string) { + const result = createFunctionFromObjectProperty(objectName, property); + Object.defineProperty(result, "wrapperName", { value: wrapperName }); + return result; +} + +import { FunctionImplementation, __requireInternal, doesImplement, implementationStores, initStores } from "../common/index.js"; +import { createJavaScriptFromObject, getKeyValue } from "../utils.js"; +import { parse } from "@babel/parser"; +import { IModImplementation } from "./ModImplementation.js"; +import { IMPLEMENTATION_STORES_PATH_REQ, IMPLEMENTATION_STORES_PATH_SOURCE, IMPLEMENTATION_STORES_PATH_VAR_NAME } from "../constants.js"; +/** + * this is really wrong, TODO: fix this piece of... garbage + */ +export async function addCode(mod: IModImplementation) { + // let rawCode = "globalThis.implementationStores = {\n"; // TODO: fix, this is awful + // for (const key in implementationStores) { + // if (Object.prototype.hasOwnProperty.call(implementationStores, key)) { + // const element = implementationStores[key].implementationStore; + // rawCode += `\t${key}: {\n`; + // for (const key2 in element) { + // if (Object.prototype.hasOwnProperty.call(element, key2)) { + // } + // } + // } + // } + await initStores(); + const constructed: { + [key: string]: { + [key: string]: FunctionImplementation; + // [key: string]: string; + } + // [key: string]: string, + } = {}; + for (const key in implementationStores) { + if (Object.prototype.hasOwnProperty.call(implementationStores, key)) { + // constructed[key] = { + // ...implementationStores[key].implementationStore, + // }; + constructed[key] = {}; + for (const key2 in implementationStores[key].implementationStore) { + if (Object.prototype.hasOwnProperty.call(implementationStores[key].implementationStore, key2)) { + const element = implementationStores[key].implementationStore[key2]; + if (doesImplement(mod, key, key2)) continue; + if (element.isWrapper === true) { + const categoryObj = getKeyValue(mod, key as keyof IModImplementation); + const unWrapped = getKeyValue(categoryObj, element.supplies as never) as any; + if (unWrapped.wrapperName != key2) continue; + } + if (element.func.toString().includes(__requireInternal.name)) { + // const regex = new RegExp(__requireInternal.name + "\\(([^)]+)\\)", 'g'); + // const match = element.func.toString().match(regex); + // if (!match) + // continue; + // const args = match[1].split(',').map(value => value.trim()); + // args.shift(); + // console.log(args); + // // const constructed2 = element.func.toString().replace(new RegExp(__requireInternal.name + "\\([^,]+,\\s*", 'g'), "globalThis.implementationStores_require(") + "}.func"; + // // element.func = new Function("return {" + constructed2)(); + // // element.func = (mod as { [key: string]: any })[args[0]][args[1]]; + const regexPattern = new RegExp(`${__requireInternal.name}\\(([^)]+)\\)`, "g"); + let match; + while ((match = regexPattern.exec(element.func.toString())) !== null) { + const argsStr = match[1]; + const args = argsStr.split(',').map(x => x.trim()).map(x => x.startsWith("\"") && x.endsWith("\"") ? x.slice(1, -1) : x); + args.shift(); + + // const replacement = "globalThis.implementationStores_require(" + someValues.join(",") + ")"; + // const result = element.func.toString().replace(match[0], replacement); + try { + const findResult = (mod as { [key: string]: any })[args[0]][args[1]]; + if (findResult.wrapperName && args[2] != "true") { + args[1] = findResult.wrapperName; + throw new Error(); + } + // const findResult = __requireInternal(mod, args[0], args[1]) as unknown as any; + // element.func = findResult; + element.func = new Function("return {" + element.func.toString().replace(match[0], `${findResult.object}.${findResult.property}`) + "}.func")(); + } + catch (error) { + // console.error(mod, args, error); + console.error((mod as { [key: string]: any })[args[0]], (mod as { [key: string]: any })[args[0]][args[1]]); + const replacement = `${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_REQ}(${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_VAR_NAME}["${args[0]}"]["${args[1]}"])`; + element.func = new Function("return {" + element.func.toString().replace(match[0], replacement) + "}.func")(); + } + } + } + constructed[key][key2] = element; + } + } + } + } + // const rawCode = "globalThis.implementationStores = {\n" + getMain(serializer).serialize(constructed) + "\n}"; + const req = (target: any) => new Function("return {" + target.func + "}.func;")(); + const rawCode = + `${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_VAR_NAME} = (${createJavaScriptFromObject(constructed, true)}); + ${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_REQ} = ${req.toString()};`; + // console.log(rawCode); + const rawCodeAst = parse(rawCode); + return rawCodeAst.program.body; +} diff --git a/src/api/Webpack.ts b/src/api/Webpack.ts index 7909bbc..21bc7bb 100644 --- a/src/api/Webpack.ts +++ b/src/api/Webpack.ts @@ -9,4 +9,4 @@ class DummyWebpackApi implements IBaseWebpackApi { } } -export const WebpackApi = new DummyWebpackApi(); \ No newline at end of file +export const WebpackApi = new DummyWebpackApi(); diff --git a/src/cli.ts b/src/cli.ts index 7af1202..9f0b2b2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,9 +4,10 @@ import { readFileSync, readdirSync, writeFileSync } from "fs"; import * as url from 'url'; import converter from "./converter.js"; import { File } from "@babel/types"; -import { myPackageName } from "./utils.js"; +import { getMain, myPackageName } from "./utils.js"; import { transformSync } from "@babel/core"; import { IModImplementation } from "./api/ModImplementation.js"; +// import { addCode } from "./api/RuntimeGenerators.js"; if (process.argv.length != 5) { console.error(`Usage:\n\t${myPackageName} \nExample:\n\t${myPackageName} ./index.js BetterDiscord ./dist/index.js`); @@ -39,13 +40,15 @@ if (!isClientModSupported) { console.error("Supported client mods: " + supportedClientMods.join(", ")); process.exit(1); } - +// eslint-disable-next-line @typescript-eslint/no-unused-vars +// addCode().then(_ => process.exit(0)); +// // process.exit(0); const filler = import(url.pathToFileURL(`${__dirname}/converters/${targetDiscordMod}.js`).href); // eslint-disable-next-line @typescript-eslint/no-unused-vars -filler.then((x: { default: IModImplementation }) => { +filler.then(async (x: { default: IModImplementation }) => { if (x.default.importsForbidden) console.warn('\x1b[33m%s\x1b[0m', `Warning: Target mod ${targetDiscordMod} requires your code to be bundled into single file`); - const out = converter(ast as File & { errors: [] }, x); + const out = await converter(ast as File & { errors: [] }, x); const outMod = { ...ast, program: { @@ -53,7 +56,7 @@ filler.then((x: { default: IModImplementation }) => { body: out, }, } as File; - const generate = typeof generate_ == "function" ? generate_ : (generate_ as { default: typeof generate_ }).default; + const generate = getMain(generate_); const final = generate(outMod).code; console.log("generated code: ", final); writeFileSync(process.argv[4], final); diff --git a/src/common/WebpackApi.ts b/src/common/WebpackApi.ts new file mode 100644 index 0000000..69ce0f0 --- /dev/null +++ b/src/common/WebpackApi.ts @@ -0,0 +1,82 @@ +import { FunctionImplementation, __requireInternal } from "./index.js"; +import { IModImplementation } from "../api/ModImplementation.js"; + +// const reservedData = {} as { [key: string]: any }; + +export let targetMod: IModImplementation; + +const implementationStore = { + getModule: new FunctionImplementation({ + depends: [], + supplies: "getModule", + // get data() { + // return reservedData[this.supplies]; + // }, + // set data(v: any) { + // reservedData[this.supplies] = v; + // }, + data: {}, + func(filter: (mod: any) => boolean) { + if (this.data.req == undefined) { + // @ts-expect-error Non-standard property + window.webpackChunkdiscord_app.push( + [[Symbol()], + {}, + (r: { c: any; }) => + this.data = this.data ?? { + req: r, + }, + ]); + // @ts-expect-error Non-standard property + window.webpackChunkdiscord_app.pop(); + } + // @ts-expect-error too lazy + return Object.values(this.data.req).find(filter)?.exports; + }, + }), + getModuleRawToExportedWrapper: new FunctionImplementation({ + depends: [], + supplies: "getModule", + data: null, + isWrapper: true, + func(filter: (mod: any) => boolean) { + const originalGetModule = __requireInternal(targetMod, "WebpackApi", "getModule", true); + return originalGetModule!((x: { exports: any; }) => Object.values(x?.exports || {})?.some(filter)); + }, + }), + getByStrings: new FunctionImplementation({ + depends: ["getModule"], + supplies: "getByStrings", + data: null, + func(...strings) { + /* __requireInternal(targetMod, "WebpackApi", "test")!(); */ + const getModule = __requireInternal(targetMod, "WebpackApi", "getModule"); + if (!getModule) + throw new Error("Unimplemented"); + return getModule((module: any) => { + if (!module?.toString || typeof (module?.toString) !== "function") return; // Not stringable + let moduleString = ""; + try { moduleString = module?.toString([]); } + catch (err) { moduleString = module?.toString(); } + if (!moduleString) return false; // Could not create string + for (const s of strings) { + if (!moduleString.includes(s)) return false; + } + return true; + }); + }, + }), + 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 new file mode 100644 index 0000000..b9a4c24 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,82 @@ +import { IModImplementation } from "../api/ModImplementation"; +import { getKeyValue } from "../utils.js"; + +export interface IFunctionImplementation { + supplies: string, + depends: string[], + data: any, + func: (...args: any[]) => any, + isWrapper?: boolean, +} +class FunctionImplementation implements IFunctionImplementation { + supplies: string; + depends: string[]; + data: any; + func: (...args: any[]) => any; + isWrapper?: boolean | undefined; + // constructor(supplies: string, depends: string[], data: any, func: (...args: any[]) => any) { + // this.supplies = supplies; + // this.depends = depends; + // this.data = data; + // this.func = func; + // } + constructor(options: IFunctionImplementation) { + const { supplies, depends, data, func, isWrapper } = 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 }); + } +} +export { + FunctionImplementation, +}; +// import * as WebpackImplementations from "./Webpack.js"; +import { createFunctionFromObjectProperty } from "../api/RuntimeGenerators.js"; +import { readdirSync } from "fs"; +import * as url from 'url'; +import * as path from 'path'; +import { IMPLEMENTATION_STORES_PATH_SOURCE, IMPLEMENTATION_STORES_PATH_VAR_NAME } from "../constants.js"; +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +export const implementationStores = { + // "Webpack": WebpackImplementations, +} as { [category: string]: { implementationStore: { [key: string]: FunctionImplementation }, targetMod: IModImplementation } }; +export async function initStores() { + const stores = readdirSync(`${__dirname}`).filter(x => !x.startsWith("index.")); + console.log(stores); + for (let index = 0; index < stores.length; index++) { + const filler = import(url.pathToFileURL(`${__dirname}/${stores[index]}`).href); + const mod = await filler; + implementationStores[path.parse(stores[index]).name] = mod; + } + console.log("done"); +} +export function doesImplement(mod: IModImplementation, category: string, method: string) { + const categoryObj = getKeyValue(mod, category as keyof IModImplementation); + return getKeyValue(categoryObj, method as never) != undefined; +} + +export function __requireInternal(mod: IModImplementation, category: string, method: string, ignoreWrappers: boolean = false) { + if (doesImplement(mod, category, method)) { + const categoryObj = getKeyValue(mod, category as keyof IModImplementation); + const result = getKeyValue(categoryObj, method as never); + if (ignoreWrappers) + return result; + else { + if (result["wrapperName"]) { + method = result["wrapperName"]; + } + else + return result; + } + } + if (implementationStores[category].targetMod !== undefined) + implementationStores[category].targetMod = mod; + const foundImplementation = implementationStores[category].implementationStore[method]; + if (foundImplementation == undefined) + return null; // depends failed + return createFunctionFromObjectProperty(`${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_VAR_NAME}.${category}`, method); +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..cdf273c --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,3 @@ +export const IMPLEMENTATION_STORES_PATH_SOURCE = "globalThis"; +export const IMPLEMENTATION_STORES_PATH_VAR_NAME = "implementationStores"; +export const IMPLEMENTATION_STORES_PATH_REQ = IMPLEMENTATION_STORES_PATH_VAR_NAME + "_require"; diff --git a/src/converter.ts b/src/converter.ts index 480b374..0ee45a8 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -1,8 +1,10 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { ParseResult } from "@babel/parser"; -import { File, Identifier, ImportDeclaration, ImportSpecifier, MemberExpression, Statement } from "@babel/types"; -import { NonFunctionType, myPackageName } from "./utils.js"; +import { File, Identifier, ImportDeclaration, ImportSpecifier, MemberExpression, Statement, callExpression, identifier, memberExpression, 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) { if (Array.isArray(ast)) { @@ -93,9 +95,7 @@ function deepFind(obj: any, path: string): K | undefined { return current; } -const getKeyValue = (obj: T, key: K): T[K] => obj[key]; - -export default function (ast: ParseResult, targetedDiscordModApiLibrary: { default: IModImplementation }): Statement[] { +export default async function (ast: ParseResult, targetedDiscordModApiLibrary: { default: IModImplementation }): Promise { const parsedBody = ast.program.body; const importStatements = parsedBody.filter(x => x.type == "ImportDeclaration"); const importAliasMap = [] as { internalName: string, codeName: string }[]; @@ -121,6 +121,7 @@ export default function (ast: ParseResult, targetedDiscordModApiLibrary: { const trueImportsToRemove = importStatements.filter((_, index) => importsToRemove.includes(index)); const parsedBodyWithoutOurImports = parsedBody.filter((item, index) => !trueImportsToRemove.includes(parsedBody[index])); + // parsedBodyWithoutOurImports.unshift(...await addCode(targetedDiscordModApiLibrary.default)); for (let index = 0; index < parsedBodyWithoutOurImports.length; index++) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const element = parsedBodyWithoutOurImports[index]; @@ -144,12 +145,43 @@ export default function (ast: ParseResult, targetedDiscordModApiLibrary: { const targetClass: IModImplementation[keyof IModImplementation] = propDesc.value ?? propDesc.get!(); // TODO: don't make value `any` if (targetClass == undefined) continue; - const replacementObject = getKeyValue(targetClass, (trueObj.property as Identifier).name as keyof typeof targetClass) as { object: string, property: string }; + if (typeof targetClass === "object" && !((trueObj.property as Identifier).name as keyof typeof targetClass in targetClass)) { + const originalObj = (trueObj.object as Identifier).name; + const originalProp = (trueObj.property as Identifier).name; + // (trueObj.object as Identifier).name = "globalThis.implementationStores_require"; // TODO: Remove hardcoded paths + // (trueObj.property as Identifier).name = (trueObj.property as Identifier).name + ".func"; + for (const prop of Object.getOwnPropertyNames(trueObj)) { + // @ts-expect-error well + delete trueObj[prop]; + } + // const newCallExpr = callExpression(memberExpression(identifier("globalThis"), identifier("implementationStores_require")), [stringLiteral(`globalThis.implementationStores["${originalObj}"]["${originalProp}"]`)]); + const newCallExpr = callExpression(memberExpression(identifier(IMPLEMENTATION_STORES_PATH_SOURCE), identifier(IMPLEMENTATION_STORES_PATH_REQ)), [ + // stringLiteral(`globalThis.implementationStores["${originalObj}"]["${originalProp}"]`), + memberExpression(memberExpression(memberExpression(identifier(IMPLEMENTATION_STORES_PATH_SOURCE), identifier(IMPLEMENTATION_STORES_PATH_VAR_NAME)), identifier(originalObj)), identifier(originalProp)), + ]); + Object.assign(trueObj, newCallExpr); + continue; + } + const replacementObject = getKeyValue(targetClass, (trueObj.property as Identifier).name as keyof typeof targetClass) as { object: string, property: string, wrapperName?: string }; + // const replacementObject = __requireInternal(targetedDiscordModApiLibrary.default, (trueObj.object as Identifier).name, (trueObj.property as Identifier).name)! as unknown as any; + if (replacementObject.wrapperName) { + const originalObj = (trueObj.object as Identifier).name; + 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(replacementObject.wrapperName)), + ]); + Object.assign(trueObj, newCallExpr); + continue; + } (trueObj.object as Identifier).name = replacementObject.object; (trueObj.property as Identifier).name = replacementObject.property; } } } + parsedBodyWithoutOurImports.unshift(...await addCode(targetedDiscordModApiLibrary.default)); if ((targetedDiscordModApiLibrary as { default: IModImplementation } & { convertFormat: (ast_: Statement[]) => Statement[] }).convertFormat == undefined) return parsedBodyWithoutOurImports; return (targetedDiscordModApiLibrary as { default: IModImplementation } & { convertFormat: (ast_: Statement[]) => Statement[] }).convertFormat(parsedBodyWithoutOurImports); diff --git a/src/converters/replugged.ts b/src/converters/replugged.ts index 6bd77c0..176df3e 100644 --- a/src/converters/replugged.ts +++ b/src/converters/replugged.ts @@ -1,12 +1,12 @@ import { ClassDeclaration, ClassMethod, ExportDefaultDeclaration, Identifier, 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 { parse } from "@babel/parser"; class RPWebpackApi implements IBaseWebpackApi { get getModule() { - return createFunctionFromObjectProperty("replugged.webpack", "getModule"); + return createFunctionWithWrapperNeeded("replugged.webpack", "getModule", "getModuleRawToExportedWrapper"); } } diff --git a/src/utils.ts b/src/utils.ts index f153415..5aa4282 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,3 +9,126 @@ export const myPackageName = (() => { // eslint-disable-next-line @typescript-eslint/ban-types export type NonFunctionType = T extends Function ? never : T; +// eslint-disable-next-line @typescript-eslint/ban-types +export type OnlyFunctionType = T extends Function ? T : never; +export const getKeyValue = (obj: T, key: K): T[K] => obj[key]; +// eslint-disable-next-line @typescript-eslint/ban-types +export const getMain = (t: T | { default: T }) => (typeof t === "function" ? t : (t as { default: T }).default); +export function escapeJsonString(str: string) { + return str.replace(/["\\]/g, '\\$&') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/\f/g, '\\f'); +} +/** + * this function tries to create valid JSON with functions embedded + */ +export function createJavaScriptFromObject(obj_: any, intend = false) { + const tree = [ + "{", + ]; + const tree_Tabs = [0]; + function enumerateObject(obj: any, tabOffset = 0, iter = 0) { + if (obj === null) { + return; + } + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + enumerateObject(obj[i], tabOffset, iter + 1); + } + } + else if (typeof obj === 'object') { + for (const key in obj) { + const descriptor = Object.getOwnPropertyDescriptor(obj, key); + if (descriptor && (descriptor.get || descriptor.set)) { + console.log(`Property ${key} is a getter/setter.`); + } + else { + tree.push(key + ": "); + tree_Tabs.push(tabOffset); + if (typeof obj[key] === 'object') { + // console.log(`Object ${key}:`); + const isArray = Array.isArray(obj[key]); + if (isArray) { + tree.push("["); + tree_Tabs.push(tabOffset); + } + else { + tree.push("{"); + tree_Tabs.push(tabOffset); + } + enumerateObject(obj[key], tabOffset + 1, iter + 1); + if (isArray) { + tree.push("],"); + tree_Tabs.push(tabOffset); + } + else { + tree.push("},"); + tree_Tabs.push(tabOffset); + } + } + else { + // console.log(`Key: ${key}, Value: ${obj[key]}`); + // tree.push(`"${obj[key]}",`.replace(/\n/g, "\\n")); + tree.push("\"" + escapeJsonString(`${obj[key]}`) + "\","); + tree_Tabs.push(tabOffset + 1); + } + } + } + } + else { + tree.push(`"${obj}",`.replace(/\n/g, "\\n")); + tree_Tabs.push(tabOffset); + } + } + enumerateObject(obj_, 1); + tree.push("}"); + tree_Tabs.push(0); + // console.log(tree); + if (!intend) { + return tree.join("\n"); + } + const final = []; + for (let index = 0; index < tree.length; index++) { + final.push(new Array(tree_Tabs[index]).fill("\t").join("") + tree[index]); + } + return final.join("\n"); +} + +export type Tree = Record | null; +type TreeFilter = string | ((tree: Tree) => boolean); + +export function findInTree( + tree: Tree, + searchFilter: TreeFilter, + args: { walkable?: string[]; ignore?: string[]; maxRecursion: number } = { maxRecursion: 100 }, +): Tree | null | undefined { + const { walkable, ignore, maxRecursion } = args; + + if (maxRecursion <= 0 || !tree) return undefined; + + if ((typeof searchFilter === "string" && typeof tree?.[searchFilter] === "function") || (typeof searchFilter === "function" && searchFilter(tree))) + return tree; + + if (!Array.isArray(tree) && typeof tree !== "object") + return undefined; + + const elements = Array.isArray(tree) ? tree : (walkable == null ? Object.keys(tree) : walkable); + + for (const element of elements) { + if (ignore?.includes(element)) continue; + + const subtree = Array.isArray(tree) ? element : tree[element]; + const result = findInTree(subtree, searchFilter, { + walkable, + ignore, + maxRecursion: maxRecursion - 1, + }); + + if (result !== undefined) + return result; + } + + return undefined; +} diff --git a/tsconfig.json b/tsconfig.json index d6c164a..81e8d69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,6 @@ "rootDir": "./src", "strict": true, // "esModuleInterop": true + "skipLibCheck": true, } }