Files
vencord-wayland-keybinds/check-patches.mjs
T
Evandro Fonseca Junior a3a9149184 WaylandKeybinds: reliable Discord global keybinds on GNOME Wayland
Vencord plugin for the Discord desktop app. Toggles fire on key press,
push-to-talk infers releases GNOME drops (re-registration probe), and
GNOME's trigger descriptions are matched correctly. Includes basis-path
tests, a patch checker against the live Discord bundle, the design spec
and the README assets.
2026-11-02 00:51:25 -03:00

73 lines
4.1 KiB
JavaScript

/*
* 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);