/* * Vencord, a Discord client mod * Copyright (c) 2026 Evandro * SPDX-License-Identifier: GPL-3.0-or-later */ // Applies this plugin's patches to a downloaded Discord bundle the way Vencord does: // unique find, one match per replacement, patched module still parses. // Usage: node check-patches.mjs [dir with discord.com/assets/*.js, optional lazy/ subdir] // Without a dir it downloads the entry bundles that discord.com/app serves right now. import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { PATCHES } from "./patches.ts"; // espree is ESLint's acorn-based parser, already installed with the repo const { parse: espreeParse } = createRequire(createRequire(import.meta.url).resolve("eslint/package.json"))("espree"); const parse = (code, opts) => espreeParse(code, { ...opts, ecmaVersion: "latest" }); const dir = process.argv[2] ?? await downloadEntryBundles(); async function downloadEntryBundles() { const out = mkdtempSync(join(tmpdir(), "discord-web-")); const html = await (await fetch("https://discord.com/app")).text(); const scripts = [...html.matchAll(/src="(\/assets\/[^"]+\.js)"/g)].map(m => m[1]); for (const src of scripts) writeFileSync(join(out, src.split("/").pop()), await (await fetch("https://discord.com" + src)).text()); console.log(`baixados ${scripts.length} bundles de discord.com/app em ${out}`); return out; } const SELF = 'Vencord.Plugins.plugins["WaylandKeybinds"]'; const canon = re => new RegExp(re.source.replaceAll(/(\\*)\\i/g, (m, esc) => esc.length % 2 === 0 ? esc + "(?:[A-Za-z_$][\\w$]*)" : m.slice(1)), re.flags); const lazy = join(dir, "lazy"); const files = readdirSync(dir).filter(f => f.endsWith(".js")).map(f => join(dir, f)) .concat(existsSync(lazy) ? readdirSync(lazy).filter(f => f.endsWith(".js")).map(f => join(lazy, f)) : []); let failed = false; const fail = msg => { failed = true; console.log("FALHOU " + msg); }; const ok = msg => console.log("ok " + msg); for (const patch of PATCHES) { const hits = files.filter(f => readFileSync(f, "utf8").includes(patch.find)); if (hits.length !== 1) { fail(`find "${patch.find}" em ${hits.length} arquivos`); continue; } const src = readFileSync(hits[0], "utf8"); const pos = src.indexOf(patch.find); let module = null; // rspack modules are methods of one big object: `123(e,t,n){...}` const walk = node => { if (!node || typeof node.type !== "string" || node.end < pos || node.start > pos) return; if (node.type === "Property" && node.value?.type === "FunctionExpression" && /^\d+$/.test(String(node.key.value ?? node.key.name))) module = node; for (const v of Object.values(node)) Array.isArray(v) ? v.forEach(walk) : walk(v); }; walk(parse(src, { ecmaVersion: "latest", sourceType: "script" })); if (!module) { fail(`módulo de "${patch.find}" não encontrado`); continue; } ok(`find "${patch.find}" único → módulo ${module.key.value ?? module.key.name} (${hits[0].split("/").pop()})`); if (src.indexOf(patch.find, pos + 1) !== -1 && src.indexOf(patch.find, pos + 1) < module.end) ok("find repete dentro do mesmo módulo (permitido)"); let code = "(function" + src.slice(module.value.start, module.value.end) + ")"; for (const { match, replace } of [patch.replacement].flat()) { const re = canon(match); const count = [...code.matchAll(new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g"))].length; if (count !== 1) { fail(`match ${match} casou ${count} vezes`); continue; } const before = code; code = code.replace(re, replace.replaceAll("$self", SELF)); const at = [...before].findIndex((c, i) => c !== code[i]); ok(`match ${match} casou 1 vez\n → ${code.slice(Math.max(0, at - 20), at + 150)}`); } try { parse(code, { ecmaVersion: "latest" }); ok("módulo corrigido continua JavaScript válido"); } catch (e) { fail("módulo corrigido não compila: " + e.message); } } process.exit(failed ? 1 : 0);