UNPKG

equicord-companion

Version:

Equicord Companion is a vscode extension to test Equicord patches & webpack finds right from the comfort of your IDE

405 lines (332 loc) 12.2 kB
import { WebpackAstParser } from "@ast/webpack"; import { format } from "@modules/format"; import { outputChannel } from "@modules/logging"; import { BufferedProgressBar, exists, getCurrentFolder, isDirectory, ProgressBar, SecTo } from "@modules/util"; import { sendAndGetData } from "@server"; import { formatModule } from "./util"; import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises"; import { join, resolve } from "path"; import { ProgressLocation, Uri, window } from "vscode"; class _ModuleCache { folder: string; baseFolder: string | undefined; get workspaceFolder() { if (this.baseFolder) return this.baseFolder; const toRet = getCurrentFolder(); if (toRet == null) { throw new Error("You are not in a folder, try opening a file"); } return toRet; } private get modpath() { return join(this.workspaceFolder, this.folder); } constructor({ folder = ".modules", baseFolder }: { folder?: string; baseFolder?: string; }) { this.baseFolder = baseFolder; this.folder = folder; } public getModuleURI(id: string) { return Uri.file(this.getModulePath(id)); } public getModulePath(id: string): string { return resolve(join(this.modpath, `${id}.js`)); } async downloadModules() { try { const before = performance.now(); const moduleIds = await this.getModuleIDs(); const modmap = await this.downloadModuleText(moduleIds); await this.formatModules(modmap); await this.writeModules(modmap); const after = performance.now(); outputChannel.debug(`Downloading ${moduleIds.length} modules took ${after - before}ms`); } catch (error) { window.showErrorMessage(`Error downloading modules:\n${String(error)}`); outputChannel.error(String(error)); } } async clearCache() { if (!await this.hasCache()) { throw new Error("No cache to clear"); } return rm(this.modpath, { recursive: true, force: false, }); } async hasCache() { return await exists(this.modpath); } public async getModuleFromNum(id: string): Promise<string> { if (!await this.hasCache()) { throw new Error("Module cache not found"); } return await readFile(join(this.modpath, `${id}.js`), { encoding: "utf-8", }); } private async writeModules(modmap: Record<string, string>) { if (await exists(this.modpath)) { throw new Error(".modules already exists, please run `equicord-companion.clearCache` first"); } await mkdir(this.modpath); let canceled = false; const progress = await new ProgressBar(Object.entries(modmap).length, "Writing modules", () => { canceled = true; }) .start(); for (const [id, text] of Object.entries(modmap)) { if (canceled) { throw new Error("Module writing canceled"); } try { // FIXME: check if id has any invalid/malicious characters await writeFile(join(this.modpath, `${id}.js`), text); progress.increment(); } catch (error) { progress.stop(error); throw error; } } } private async formatModules(modmap: Record<string, string>) { let canceled = false; const progress = await new BufferedProgressBar(Object.entries(modmap).length, "Formatting modules", () => { canceled = true; }) .start(); const startTime = performance.now(); for (const [id, text] of Object.entries(modmap)) { if (canceled) { throw new Error("Module formatting canceled"); } modmap[id] = format(formatModule(text, id)); await progress.increment(); } const endTime = performance.now(); outputChannel.debug(`Formatting modules took ${endTime - startTime}ms`); } private async downloadModuleText(moduleIDs: string[]) { let isCancelled = false; const progress = await new ProgressBar(moduleIDs.length, "Downloading modules", () => { isCancelled = true; }) .start(); const res: Record<string, string> = {}; for (const id of moduleIDs) { if (isCancelled) { throw new Error("Module download canceled"); break; } progress.increment(); let text: string; try { [{ data: { module: text } }] = [ await sendAndGetData<"extract">({ type: "extract", data: { extractType: "id", idOrSearch: +id, usePatched: null, }, }), ]; } catch (error) { progress.stop(error); throw error; break; } res[id] = text; } return res; } private async getModuleIDs() { const allModules = await sendAndGetData<"moduleList">({ type: "allModules", data: null, }, { timeout: 120 * SecTo.MS, }); return allModules.data.modules; } } const MODULE_ID_FILE_REGEX = /(\d+)\.js/; type DepsGeneratorOpts = | { modmap: Record<string, string>; } | { fromDisk: true; folder?: string; baseFolder?: string; }; type AllDeps = Record<string, { /** * the modules that require this module syncranously */ syncUses: string[]; /** * the modules that require this module lazily */ lazyUses: string[]; }>; export class ModuleDepManager { private static modCache: AllDeps | null = null; private modmap?: Record<string, string>; currentFolder: string; moduleFolder: string; static DEFAULT_FOLDER = ".modules"; static CACHE_FILE_NAME = "cache.json"; public static getModDeps(moduleid: string) { if (this.hasModDeps()) { return this.modCache![moduleid]; } throw new Error("Module Deps not initialized"); } public static hasModDeps() { return !!this.modCache; } // FIXME: setting to start caching when a webpack module is opened / when the equicord workspace is opened public static async initModDeps(opts: DepsGeneratorOpts) { this.modCache = await new this(opts) .gererateDeps(); } constructor(opts: DepsGeneratorOpts) { this.currentFolder = ("baseFolder" in opts && opts.baseFolder) || getCurrentFolder()!; this.moduleFolder = ("folder" in opts && opts.folder) || ModuleDepManager.DEFAULT_FOLDER; if (this.currentFolder == null) throw new Error("No folder found, please make sure you are in a workspace"); if ("modmap" in opts) { this.modmap = opts.modmap; } } public async gererateDeps(): Promise<AllDeps> { // check if the deps are cached first, if so, load them if (await exists(join(this.currentFolder, this.moduleFolder, ModuleDepManager.CACHE_FILE_NAME))) { const file = await readFile(join(this.currentFolder, this.moduleFolder, ModuleDepManager.CACHE_FILE_NAME), "utf-8"); const cachedData = JSON.parse(file) as AllDeps; return cachedData; } // FIXME: horror const toRet: AllDeps = ModuleDepManager.makeDepsMap(); let cancelled = false; const progress = await new BufferedProgressBar(Object.entries(await this.getModmap()).length, "Parsing Modules", () => { cancelled = true; }) .start(); for (const [id, text] of Object.entries(await this.getModmap())) { if (cancelled) { throw new Error("canceled by user"); } try { const deps = new WebpackAstParser(text) .getModulesThatThisModuleRequires(); for (const syncDep of deps?.sync ?? []) { toRet[syncDep].syncUses.push(id); } for (const lazyDep of deps?.lazy ?? []) { toRet[lazyDep].lazyUses.push(id); } await progress.increment(); } catch (e) { progress.stop(e); throw e; } } // write the deps to cache const deps = JSON.stringify(toRet); writeFile(join(this.currentFolder, this.moduleFolder, ModuleDepManager.CACHE_FILE_NAME), deps, "utf-8") .catch((e) => { outputChannel.error(`Failed to write cached module deps to disk. Error: ${e}`); }); return toRet; } private static makeDepsMap(): AllDeps { const target = {}; return new Proxy(target, { get(target, prop, rec) { if (typeof prop === "string" && prop.match(/\d+/)) { if (!Reflect.has(target, prop)) { const val = ({ lazyUses: [], syncUses: [], } satisfies AllDeps[string]); Reflect.set(target, prop, val, rec); return val; } } return Reflect.get(target, prop, rec); }, }); } protected async getModmap(): Promise<Record<string, string>> { if (this.modmap) { return this.modmap; } const toRet = {}; const modpath = join(this.currentFolder, this.moduleFolder); const validPath = await exists(modpath) && await isDirectory(modpath); if (!validPath) throw new Error(`modpath is not valid. got: ${modpath}`); const files = await ProgressBar.forSingleFunc({ location: ProgressLocation.Notification, cancellable: true, title: "reading module list", }, () => readdir(modpath)); let cancelled = false; const progress = await new ProgressBar(files.length, "loading files", () => { cancelled = true; }) .start(); for (const file of files) { if (cancelled) { throw new Error("reading files cancelled by user"); } try { const filepath = join(modpath, file); if (await isDirectory(filepath)) { progress.increment(); continue; } const modId = file.match(MODULE_ID_FILE_REGEX)?.[1]; if (!modId) { progress.increment(); continue; } progress.increment(); // FIXME: add abort singal const text = await readFile(filepath, { encoding: "utf-8", }); toRet[modId] = text; } catch (error) { progress.stop(error); throw error; } } return (this.modmap = toRet); } } export class testProgressBar { constructor() { } async start() { const timeouts: NodeJS.Timeout[] = []; const bar = new ProgressBar(4, "testing abc", () => { timeouts.map(clearTimeout); window.showInformationMessage("Canceled"); }); await bar.start(); timeouts.push( setTimeout(() => bar.increment(), 0), setTimeout(() => bar.increment(), 1000), setTimeout(() => bar.increment(), 2000), setTimeout(() => bar.increment(), 3000), ); } } export const ModuleCache = new _ModuleCache({ folder: ".modules", });