Merge branch 'dev' into main

This commit is contained in:
byeoon 2024-05-30 09:34:31 -04:00 committed by GitHub
commit adc60648a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 331 additions and 187 deletions

View file

@ -37,8 +37,8 @@ jobs:
with: with:
chrome-version: stable chrome-version: stable
- name: Build web - name: Build Vencord Reporter Version
run: pnpm buildWeb --standalone --dev run: pnpm buildReporter
- name: Create Report - name: Create Report
timeout-minutes: 10 timeout-minutes: 10

View file

@ -20,7 +20,9 @@
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"buildStandalone": "pnpm build --standalone", "buildStandalone": "pnpm build --standalone",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"buildReporter": "pnpm buildWeb --standalone --reporter --skip-extension",
"watch": "pnpm build --watch", "watch": "pnpm build --watch",
"watchWeb": "pnpm buildWeb --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts", "generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types", "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs", "inject": "node scripts/runInstaller.mjs",

View file

@ -21,19 +21,21 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises"; import { readdir } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, VERSION, watch } from "./common.mjs";
const defines = { const defines = {
IS_STANDALONE: isStandalone, IS_STANDALONE,
IS_DEV: JSON.stringify(isDev), IS_DEV,
IS_UPDATER_DISABLED: updaterDisabled, IS_REPORTER,
IS_UPDATER_DISABLED,
IS_WEB: false, IS_WEB: false,
IS_EXTENSION: false, IS_EXTENSION: false,
VERSION: JSON.stringify(VERSION), VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP, BUILD_TIMESTAMP
}; };
if (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimise if (defines.IS_STANDALONE === false)
// If this is a local build (not standalone), optimize
// for the specific platform we're on // for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform); defines["process.platform"] = JSON.stringify(process.platform);
@ -46,7 +48,7 @@ const nodeCommonOpts = {
platform: "node", platform: "node",
target: ["esnext"], target: ["esnext"],
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external], external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
define: defines, define: defines
}; };
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`; const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
@ -73,13 +75,13 @@ const globNativesPlugin = {
let i = 0; let i = 0;
for (const dir of pluginDirs) { for (const dir of pluginDirs) {
const dirPath = join("src", dir); const dirPath = join("src", dir);
if (!await existsAsync(dirPath)) continue; if (!await exists(dirPath)) continue;
const plugins = await readdir(dirPath); const plugins = await readdir(dirPath);
for (const p of plugins) { for (const p of plugins) {
const nativePath = join(dirPath, p, "native.ts"); const nativePath = join(dirPath, p, "native.ts");
const indexNativePath = join(dirPath, p, "native/index.ts"); const indexNativePath = join(dirPath, p, "native/index.ts");
if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath))) if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
continue; continue;
const nameParts = p.split("."); const nameParts = p.split(".");

View file

@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path"; import { join } from "path";
import Zip from "zip-local"; import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
/** /**
* @type {esbuild.BuildOptions} * @type {esbuild.BuildOptions}
@ -40,15 +40,16 @@ const commonOptions = {
], ],
target: ["esnext"], target: ["esnext"],
define: { define: {
IS_WEB: "true", IS_WEB: true,
IS_EXTENSION: "false", IS_EXTENSION: false,
IS_STANDALONE: "true", IS_STANDALONE: true,
IS_DEV: JSON.stringify(isDev), IS_DEV,
IS_DISCORD_DESKTOP: "false", IS_REPORTER,
IS_VESKTOP: "false", IS_DISCORD_DESKTOP: false,
IS_UPDATER_DISABLED: "true", IS_VESKTOP: false,
IS_UPDATER_DISABLED: true,
VERSION: JSON.stringify(VERSION), VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP, BUILD_TIMESTAMP
} }
}; };
@ -87,16 +88,16 @@ await Promise.all(
esbuild.build({ esbuild.build({
...commonOptions, ...commonOptions,
outfile: "dist/browser.js", outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }, footer: { js: "//# sourceURL=VencordWeb" }
}), }),
esbuild.build({ esbuild.build({
...commonOptions, ...commonOptions,
outfile: "dist/extension.js", outfile: "dist/extension.js",
define: { define: {
...commonOptions?.define, ...commonOptions?.define,
IS_EXTENSION: "true", IS_EXTENSION: true,
}, },
footer: { js: "//# sourceURL=VencordWeb" }, footer: { js: "//# sourceURL=VencordWeb" }
}), }),
esbuild.build({ esbuild.build({
...commonOptions, ...commonOptions,
@ -112,7 +113,7 @@ await Promise.all(
footer: { footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});" js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
}, }
}) })
] ]
); );
@ -165,7 +166,7 @@ async function buildExtension(target, files) {
f.startsWith("manifest") ? "manifest.json" : f, f.startsWith("manifest") ? "manifest.json" : f,
content content
]; ];
}))), })))
}; };
await rm(target, { recursive: true, force: true }); await rm(target, { recursive: true, force: true });
@ -192,14 +193,19 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
return appendFile("dist/Vencord.user.js", cssRuntime); return appendFile("dist/Vencord.user.js", cssRuntime);
}); });
await Promise.all([ if (!process.argv.includes("--skip-extension")) {
await Promise.all([
appendCssRuntime, appendCssRuntime,
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]), buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]), buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
]); ]);
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip"); Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip"); console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip"); Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip"); console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
} else {
await appendCssRuntime;
}

View file

@ -35,24 +35,26 @@ const PackageJSON = JSON.parse(readFileSync("package.json"));
export const VERSION = PackageJSON.version; export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/ // https://reproducible-builds.org/docs/source-date-epoch/
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now(); export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
export const watch = process.argv.includes("--watch"); export const watch = process.argv.includes("--watch");
export const isDev = watch || process.argv.includes("--dev"); export const IS_DEV = watch || process.argv.includes("--dev");
export const isStandalone = JSON.stringify(process.argv.includes("--standalone")); export const IS_REPORTER = process.argv.includes("--reporter");
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater")); export const IS_STANDALONE = process.argv.includes("--standalone");
export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater");
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim(); export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
export const banner = { export const banner = {
js: ` js: `
// Vencord ${gitHash} // Vencord ${gitHash}
// Standalone: ${isStandalone} // Standalone: ${IS_STANDALONE}
// Platform: ${isStandalone === "false" ? process.platform : "Universal"} // Platform: ${IS_STANDALONE === false ? process.platform : "Universal"}
// Updater disabled: ${updaterDisabled} // Updater Disabled: ${IS_UPDATER_DISABLED}
`.trim() `.trim()
}; };
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs")); export async function exists(path) {
return await access(path, FsConstants.F_OK)
export function existsAsync(path) {
return access(path, FsConstants.F_OK)
.then(() => true) .then(() => true)
.catch(() => false); .catch(() => false);
} }
@ -66,7 +68,7 @@ export const makeAllPackagesExternalPlugin = {
setup(build) { setup(build) {
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../" const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true })); build.onResolve({ filter }, args => ({ path: args.path, external: true }));
}, }
}; };
/** /**
@ -89,14 +91,14 @@ export const globPlugins = kind => ({
let plugins = "\n"; let plugins = "\n";
let i = 0; let i = 0;
for (const dir of pluginDirs) { for (const dir of pluginDirs) {
if (!await existsAsync(`./src/${dir}`)) continue; if (!await exists(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`); const files = await readdir(`./src/${dir}`);
for (const file of files) { for (const file of files) {
if (file.startsWith("_") || file.startsWith(".")) continue; if (file.startsWith("_") || file.startsWith(".")) continue;
if (file === "index.ts") continue; if (file === "index.ts") continue;
const target = getPluginTarget(file); const target = getPluginTarget(file);
if (target) { if (target && !IS_REPORTER) {
if (target === "dev" && !watch) continue; if (target === "dev" && !watch) continue;
if (target === "web" && kind === "discordDesktop") continue; if (target === "web" && kind === "discordDesktop") continue;
if (target === "desktop" && kind === "web") continue; if (target === "desktop" && kind === "web") continue;
@ -178,7 +180,7 @@ export const fileUrlPlugin = {
build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => { build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
const { searchParams } = new URL(uri); const { searchParams } = new URL(uri);
const base64 = searchParams.has("base64"); const base64 = searchParams.has("base64");
const minify = isStandalone === "true" && searchParams.has("minify"); const minify = IS_STANDALONE === true && searchParams.has("minify");
const noTrim = searchParams.get("trim") === "false"; const noTrim = searchParams.get("trim") === "false";
const encoding = base64 ? "base64" : "utf-8"; const encoding = base64 ? "base64" : "utf-8";

View file

@ -205,7 +205,12 @@ page.on("console", async e => {
} }
if (isVencord) { if (isVencord) {
const args = await Promise.all(e.args().map(a => a.jsonValue())); let args: unknown[] = [];
try {
args = await Promise.all(e.args().map(a => a.jsonValue()));
} catch {
return;
}
const [, tag, message] = args as Array<string>; const [, tag, message] = args as Array<string>;
const cause = await maybeGetError(e.args()[3]); const cause = await maybeGetError(e.args()[3]);
@ -277,7 +282,7 @@ page.on("pageerror", e => console.error("[Page Error]", e));
await page.setBypassCSP(true); await page.setBypassCSP(true);
async function runtime(token: string) { async function reporterRuntime(token: string) {
console.log("[PUP_DEBUG]", "Starting test..."); console.log("[PUP_DEBUG]", "Starting test...");
try { try {
@ -285,43 +290,7 @@ async function runtime(token: string) {
Object.defineProperty(navigator, "languages", { Object.defineProperty(navigator, "languages", {
get: function () { get: function () {
return ["en-US", "en"]; return ["en-US", "en"];
},
});
// Monkey patch Logger to not log with custom css
// @ts-ignore
const originalLog = Vencord.Util.Logger.prototype._log;
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
if (level === "warn" || level === "error")
return console[level]("[Vencord]", this.name + ":", ...args);
return originalLog.call(this, level, levelColor, args);
};
// Force enable all plugins and patches
Vencord.Plugins.patches.length = 0;
Object.values(Vencord.Plugins.plugins).forEach(p => {
// Needs native server to run
if (p.name === "WebRichPresence (arRPC)") return;
Vencord.Settings.plugins[p.name].enabled = true;
p.patches?.forEach(patch => {
patch.plugin = p.name;
delete patch.predicate;
delete patch.group;
Vencord.Util.canonicalizeFind(patch);
if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement];
} }
patch.replacement.forEach(r => {
delete r.predicate;
});
Vencord.Plugins.patches.push(patch);
});
}); });
let wreq: typeof Vencord.Webpack.wreq; let wreq: typeof Vencord.Webpack.wreq;
@ -338,7 +307,7 @@ async function runtime(token: string) {
// True if resolved, false otherwise // True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>; const chunksSearchPromises = [] as Array<() => boolean>;
const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
async function searchAndLoadLazyChunks(factoryCode: string) { async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(LazyChunkRegex); const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
@ -348,8 +317,7 @@ async function runtime(token: string) {
// the chunk containing the component // the chunk containing the component
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => { await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : []; const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
if (chunkIds.length === 0) { if (chunkIds.length === 0) {
@ -520,14 +488,14 @@ async function runtime(token: string) {
} else if (method === "extractAndLoadChunks") { } else if (method === "extractAndLoadChunks") {
const [code, matcher] = args; const [code, matcher] = args;
const module = Vencord.Webpack.findModuleFactory(...code); result = await Vencord.Webpack.extractAndLoadChunks(code, matcher);
if (module) result = module.toString().match(canonicalizeMatch(matcher)); if (result === false) result = null;
} else { } else {
// @ts-ignore // @ts-ignore
result = Vencord.Webpack[method](...args); result = Vencord.Webpack[method](...args);
} }
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
} catch (e) { } catch (e) {
let logMessage = searchType; let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
@ -545,9 +513,10 @@ async function runtime(token: string) {
} }
await page.evaluateOnNewDocument(` await page.evaluateOnNewDocument(`
${readFileSync("./dist/browser.js", "utf-8")} if (location.host.endsWith("discord.com")) {
${readFileSync("./dist/browser.js", "utf-8")};
;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
}
`); `);
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login"); await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");

29
src/api/MessageUpdater.ts Normal file
View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { MessageCache, MessageStore } from "@webpack/common";
import { FluxStore } from "@webpack/types";
import { Message } from "discord-types/general";
/**
* Update and re-render a message
* @param channelId The channel id of the message
* @param messageId The message id
* @param fields The fields of the message to change. Leave empty if you just want to re-render
*/
export function updateMessage(channelId: string, messageId: string, fields?: Partial<Message>) {
const channelMessageCache = MessageCache.getOrCreate(channelId);
if (!channelMessageCache.has(messageId)) return;
// To cause a message to re-render, we basically need to create a new instance of the message and obtain a new reference
// If we have fields to modify we can use the merge method of the class, otherwise we just create a new instance with the old fields
const newChannelMessageCache = channelMessageCache.update(messageId, (oldMessage: any) => {
return fields ? oldMessage.merge(fields) : new oldMessage.constructor(oldMessage);
});
MessageCache.commit(newChannelMessageCache);
(MessageStore as unknown as FluxStore).emitChange();
}

View file

@ -26,6 +26,7 @@ import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageDecorations from "./MessageDecorations"; import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover"; import * as $MessagePopover from "./MessagePopover";
import * as $MessageUpdater from "./MessageUpdater";
import * as $Notices from "./Notices"; import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications"; import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
@ -110,3 +111,8 @@ export const ContextMenu = $ContextMenu;
* An API allowing you to add buttons to the chat input * An API allowing you to add buttons to the chat input
*/ */
export const ChatButtons = $ChatButtons; export const ChatButtons = $ChatButtons;
/**
* An API allowing you to update and re-render messages
*/
export const MessageUpdater = $MessageUpdater;

View file

@ -18,14 +18,14 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
if (IS_DEV) { if (IS_DEV || IS_REPORTER) {
var traces = {} as Record<string, [number, any[]]>; var traces = {} as Record<string, [number, any[]]>;
var logger = new Logger("Tracer", "#FFD166"); var logger = new Logger("Tracer", "#FFD166");
} }
const noop = function () { }; const noop = function () { };
export const beginTrace = !IS_DEV ? noop : export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
function beginTrace(name: string, ...args: any[]) { function beginTrace(name: string, ...args: any[]) {
if (name in traces) if (name in traces)
throw new Error(`Trace ${name} already exists!`); throw new Error(`Trace ${name} already exists!`);
@ -33,7 +33,7 @@ export const beginTrace = !IS_DEV ? noop :
traces[name] = [performance.now(), args]; traces[name] = [performance.now(), args];
}; };
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) { export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
const end = performance.now(); const end = performance.now();
const [start, args] = traces[name]; const [start, args] = traces[name];
@ -48,7 +48,7 @@ type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
const noopTracer = const noopTracer =
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f; <F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
export const traceFunction = !IS_DEV export const traceFunction = !(IS_DEV || IS_REPORTER)
? noopTracer ? noopTracer
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F { : function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
return function (this: any, ...args: Parameters<F>) { return function (this: any, ...args: Parameters<F>) {

3
src/globals.d.ts vendored
View file

@ -34,9 +34,10 @@ declare global {
*/ */
export var IS_WEB: boolean; export var IS_WEB: boolean;
export var IS_EXTENSION: boolean; export var IS_EXTENSION: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean; export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean; export var IS_UPDATER_DISABLED: boolean;
export var IS_DEV: boolean;
export var IS_REPORTER: boolean;
export var IS_DISCORD_DESKTOP: boolean; export var IS_DISCORD_DESKTOP: boolean;
export var IS_VESKTOP: boolean; export var IS_VESKTOP: boolean;
export var VERSION: string; export var VERSION: string;

View file

@ -0,0 +1,37 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MessageUpdaterAPI",
description: "API for updating and re-rendering messages.",
authors: [Devs.Nuckyz],
patches: [
{
// Message accessories have a custom logic to decide if they should render again, so we need to make it not ignore changed message reference
find: "}renderEmbeds(",
replacement: {
match: /(?<=this.props,\i,\[)"message",/,
replace: ""
}
}
]
});

View file

@ -19,7 +19,7 @@
import { popNotice, showNotice } from "@api/Notices"; import { popNotice, showNotice } from "@api/Notices";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin, { ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common"; import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
@ -41,6 +41,7 @@ export default definePlugin({
name: "WebRichPresence (arRPC)", name: "WebRichPresence (arRPC)",
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)", description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
authors: [Devs.Ducko], authors: [Devs.Ducko],
reporterTestable: ReporterTestable.None,
settingsAboutComponent: () => ( settingsAboutComponent: () => (
<> <>

View file

@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { filters, findAll, search } from "@webpack"; import { filters, findAll, search } from "@webpack";
const PORT = 8485; const PORT = 8485;
@ -243,6 +243,7 @@ export default definePlugin({
name: "DevCompanion", name: "DevCompanion",
description: "Dev Companion Plugin", description: "Dev Companion Plugin",
authors: [Devs.Ven], authors: [Devs.Ven],
reporterTestable: ReporterTestable.None,
settings, settings,
toolboxActions: { toolboxActions: {

View file

@ -333,7 +333,7 @@ export default definePlugin({
] ]
}, },
{ {
find: "renderEmbeds(", find: "}renderEmbeds(",
replacement: [ replacement: [
{ {
// Call our function to decide whether the embed should be ignored or not // Call our function to decide whether the embed should be ignored or not

View file

@ -21,7 +21,7 @@ import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeFind } from "@utils/patches"; import { canonicalizeFind } from "@utils/patches";
import { Patch, Plugin, StartAt } from "@utils/types"; import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
@ -39,35 +39,68 @@ export const patches = [] as Patch[];
let enabledPluginsSubscribedFlux = false; let enabledPluginsSubscribedFlux = false;
const subscribedFluxEventsPlugins = new Set<string>(); const subscribedFluxEventsPlugins = new Set<string>();
const pluginsValues = Object.values(Plugins);
const settings = Settings.plugins; const settings = Settings.plugins;
export function isPluginEnabled(p: string) { export function isPluginEnabled(p: string) {
return ( return (
IS_REPORTER ||
Plugins[p]?.required || Plugins[p]?.required ||
Plugins[p]?.isDependency || Plugins[p]?.isDependency ||
settings[p]?.enabled settings[p]?.enabled
) ?? false; ) ?? false;
} }
const pluginsValues = Object.values(Plugins); export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
const patch = newPatch as Patch;
patch.plugin = pluginName;
// First roundtrip to mark and force enable dependencies (only for enabled plugins) if (IS_REPORTER) {
delete patch.predicate;
delete patch.group;
}
canonicalizeFind(patch);
if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement];
}
if (IS_REPORTER) {
patch.replacement.forEach(r => {
delete r.predicate;
});
}
patches.push(patch);
}
function isReporterTestable(p: Plugin, part: ReporterTestable) {
return p.reporterTestable == null
? true
: (p.reporterTestable & part) === part;
}
// First round-trip to mark and force enable dependencies
// //
// FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only // FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only
// goes for the top level and their children, but for now this works okay with the current API plugins // goes for the top level and their children, but for now this works okay with the current API plugins
for (const p of pluginsValues) if (settings[p.name]?.enabled) { for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
p.dependencies?.forEach(d => { p.dependencies?.forEach(d => {
const dep = Plugins[d]; const dep = Plugins[d];
if (dep) {
if (!dep) {
const error = new Error(`Plugin ${p.name} has unresolved dependency ${d}`);
if (IS_DEV) {
throw error;
}
logger.warn(error);
return;
}
settings[d].enabled = true; settings[d].enabled = true;
dep.isDependency = true; dep.isDependency = true;
}
else {
const error = new Error(`Plugin ${p.name} has unresolved dependency ${d}`);
if (IS_DEV)
throw error;
logger.warn(error);
}
}); });
} }
@ -82,23 +115,18 @@ for (const p of pluginsValues) {
} }
if (p.patches && isPluginEnabled(p.name)) { if (p.patches && isPluginEnabled(p.name)) {
if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) {
for (const patch of p.patches) { for (const patch of p.patches) {
patch.plugin = p.name; addPatch(patch, p.name);
canonicalizeFind(patch);
if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement];
} }
patches.push(patch);
} }
} }
} }
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins(target: StartAt) { export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins(target: StartAt) {
logger.info(`Starting plugins (stage ${target})`); logger.info(`Starting plugins (stage ${target})`);
for (const name in Plugins) for (const name in Plugins) {
if (isPluginEnabled(name)) { if (isPluginEnabled(name) && (!IS_REPORTER || isReporterTestable(Plugins[name], ReporterTestable.Start))) {
const p = Plugins[name]; const p = Plugins[name];
const startAt = p.startAt ?? StartAt.WebpackReady; const startAt = p.startAt ?? StartAt.WebpackReady;
@ -106,30 +134,38 @@ export const startAllPlugins = traceFunction("startAllPlugins", function startAl
startPlugin(Plugins[name]); startPlugin(Plugins[name]);
} }
}
}); });
export function startDependenciesRecursive(p: Plugin) { export function startDependenciesRecursive(p: Plugin) {
let restartNeeded = false; let restartNeeded = false;
const failures: string[] = []; const failures: string[] = [];
p.dependencies?.forEach(dep => {
if (!Settings.plugins[dep].enabled) { p.dependencies?.forEach(d => {
startDependenciesRecursive(Plugins[dep]); if (!settings[d].enabled) {
const dep = Plugins[d];
startDependenciesRecursive(dep);
// If the plugin has patches, don't start the plugin, just enable it. // If the plugin has patches, don't start the plugin, just enable it.
Settings.plugins[dep].enabled = true; settings[d].enabled = true;
if (Plugins[dep].patches) { dep.isDependency = true;
logger.warn(`Enabling dependency ${dep} requires restart.`);
if (dep.patches) {
logger.warn(`Enabling dependency ${d} requires restart.`);
restartNeeded = true; restartNeeded = true;
return; return;
} }
const result = startPlugin(Plugins[dep]);
if (!result) failures.push(dep); const result = startPlugin(dep);
if (!result) failures.push(d);
} }
}); });
return { restartNeeded, failures }; return { restartNeeded, failures };
} }
export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) { export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {
if (p.flux && !subscribedFluxEventsPlugins.has(p.name)) { if (p.flux && !subscribedFluxEventsPlugins.has(p.name) && (!IS_REPORTER || isReporterTestable(p, ReporterTestable.FluxEvents))) {
subscribedFluxEventsPlugins.add(p.name); subscribedFluxEventsPlugins.add(p.name);
logger.debug("Subscribing to flux events of plugin", p.name); logger.debug("Subscribing to flux events of plugin", p.name);

View file

@ -18,12 +18,13 @@
import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addChatBarButton, ChatBarButton } from "@api/ChatButtons";
import { addButton, removeButton } from "@api/MessagePopover"; import { addButton, removeButton } from "@api/MessagePopover";
import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getStegCloak } from "@utils/dependencies"; import { getStegCloak } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { ChannelStore, Constants, RestAPI, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal"; import { buildDecModal } from "./components/DecryptionModal";
@ -103,7 +104,10 @@ export default definePlugin({
name: "InvisibleChat", name: "InvisibleChat",
description: "Encrypt your Messages in a non-suspicious way!", description: "Encrypt your Messages in a non-suspicious way!",
authors: [Devs.SammCheese], authors: [Devs.SammCheese],
dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"], dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"],
reporterTestable: ReporterTestable.Patches,
settings,
patches: [ patches: [
{ {
// Indicator // Indicator
@ -120,7 +124,6 @@ export default definePlugin({
URL_REGEX: new RegExp( URL_REGEX: new RegExp(
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
), ),
settings,
async start() { async start() {
addButton("InvisibleChat", message => { addButton("InvisibleChat", message => {
return this.INV_REGEX.test(message?.content) return this.INV_REGEX.test(message?.content)
@ -180,14 +183,7 @@ export default definePlugin({
message.embeds.push(embed); message.embeds.push(embed);
} }
this.updateMessage(message); updateMessage(message.channel_id, message.id, { embeds: message.embeds });
},
updateMessage: (message: any) => {
FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
message,
});
}, },
popOverIcon: () => <PopOverIcon />, popOverIcon: () => <PopOverIcon />,

View file

@ -17,6 +17,7 @@
*/ */
import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js"; import { Devs } from "@utils/constants.js";
@ -28,7 +29,6 @@ import {
Button, Button,
ChannelStore, ChannelStore,
Constants, Constants,
FluxDispatcher,
GuildStore, GuildStore,
IconUtils, IconUtils,
MessageStore, MessageStore,
@ -250,15 +250,9 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
if (linkedMessage) { if (linkedMessage) {
messageCache.set(messageID, { message: linkedMessage, fetched: true }); messageCache.set(messageID, { message: linkedMessage, fetched: true });
} else { } else {
const msg = { ...message } as any;
delete msg.embeds;
delete msg.interaction;
messageFetchQueue.unshift(() => fetchMessage(channelID, messageID) messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
.then(m => m && FluxDispatcher.dispatch({ .then(m => m && updateMessage(message.channel_id, message.id))
type: "MESSAGE_UPDATE",
message: msg
}))
); );
continue; continue;
} }
@ -367,7 +361,7 @@ export default definePlugin({
name: "MessageLinkEmbeds", name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message", description: "Adds a preview to messages that link another message",
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev], authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
dependencies: ["MessageAccessoriesAPI"], dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI"],
settings, settings,

View file

@ -20,7 +20,7 @@ import "./shiki.css";
import { enableStyle } from "@api/Styles"; import { enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin, { ReporterTestable } from "@utils/types";
import previewExampleText from "file://previewExample.tsx"; import previewExampleText from "file://previewExample.tsx";
import { shiki } from "./api/shiki"; import { shiki } from "./api/shiki";
@ -34,6 +34,9 @@ export default definePlugin({
name: "ShikiCodeblocks", name: "ShikiCodeblocks",
description: "Brings vscode-style codeblocks into Discord, powered by Shiki", description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
authors: [Devs.Vap], authors: [Devs.Vap],
reporterTestable: ReporterTestable.Patches,
settings,
patches: [ patches: [
{ {
find: "codeBlock:{react(", find: "codeBlock:{react(",
@ -66,7 +69,6 @@ export default definePlugin({
isPreview: true, isPreview: true,
tempSettings, tempSettings,
}), }),
settings,
// exports // exports
shiki, shiki,

View file

@ -22,7 +22,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text"; import { wordsToTitle } from "@utils/text";
import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types"; import definePlugin, { OptionType, PluginOptionsItem, ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common"; import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
@ -155,6 +155,7 @@ export default definePlugin({
name: "VcNarrator", name: "VcNarrator",
description: "Announces when users join, leave, or move voice channels via narrator", description: "Announces when users join, leave, or move voice channels via narrator",
authors: [Devs.Ven], authors: [Devs.Ven],
reporterTestable: ReporterTestable.None,
flux: { flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {

View file

@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components"; import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, PluginNative } from "@utils/types"; import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common"; import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general"; import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
@ -143,7 +143,9 @@ export default definePlugin({
description: "Forwards discord notifications to XSOverlay, for easy viewing in VR", description: "Forwards discord notifications to XSOverlay, for easy viewing in VR",
authors: [Devs.Nyako], authors: [Devs.Nyako],
tags: ["vr", "notify"], tags: ["vr", "notify"],
reporterTestable: ReporterTestable.None,
settings, settings,
flux: { flux: {
CALL_UPDATE({ call }: { call: Call; }) { CALL_UPDATE({ call }: { call: Call; }) {
if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) { if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {

View file

@ -32,6 +32,11 @@ export class Logger {
constructor(public name: string, public color: string = "white") { } constructor(public name: string, public color: string = "white") { }
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") { private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
if (IS_REPORTER && (level === "warn" || level === "error")) {
console[level]("[Vencord]", this.name + ":", ...args);
return;
}
console[level]( console[level](
`%c Vencord %c %c ${this.name} ${customFmt}`, `%c Vencord %c %c ${this.name} ${customFmt}`,
`background: ${levelColor}; color: black; font-weight: bold; border-radius: 5px;`, `background: ${levelColor}; color: black; font-weight: bold; border-radius: 5px;`,

View file

@ -94,6 +94,10 @@ export interface PluginDef {
* @default StartAt.WebpackReady * @default StartAt.WebpackReady
*/ */
startAt?: StartAt, startAt?: StartAt,
/**
* Which parts of the plugin can be tested by the reporter. Defaults to all parts
*/
reporterTestable?: number;
/** /**
* Optionally provide settings that the user can configure in the Plugins tab of settings. * Optionally provide settings that the user can configure in the Plugins tab of settings.
* @deprecated Use `settings` instead * @deprecated Use `settings` instead
@ -144,6 +148,13 @@ export const enum StartAt {
WebpackReady = "WebpackReady" WebpackReady = "WebpackReady"
} }
export const enum ReporterTestable {
None = 1 << 1,
Start = 1 << 2,
Patches = 1 << 3,
FluxEvents = 1 << 4
}
export const enum OptionType { export const enum OptionType {
STRING, STRING,
NUMBER, NUMBER,

View file

@ -22,7 +22,7 @@ import { LazyComponent } from "@utils/react";
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack"; import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack";
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T { export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T {
if (IS_DEV) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
let myValue: T = function () { let myValue: T = function () {
throw new Error(`Vencord could not find the ${name} Component`); throw new Error(`Vencord could not find the ${name} Component`);
@ -38,7 +38,7 @@ export function waitForComponent<T extends React.ComponentType<any> = React.Comp
} }
export function waitForStore(name: string, cb: (v: any) => void) { export function waitForStore(name: string, cb: (v: any) => void) {
if (IS_DEV) lazyWebpackSearchHistory.push(["waitForStore", [name]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForStore", [name]]);
waitFor(filters.byStoreName(name), cb, { isIndirect: true }); waitFor(filters.byStoreName(name), cb, { isIndirect: true });
} }

View file

@ -41,8 +41,33 @@ export class FluxStore {
__getLocalVars(): Record<string, any>; __getLocalVars(): Record<string, any>;
} }
export class FluxEmitter {
constructor();
changeSentinel: number;
changedStores: Set<FluxStore>;
isBatchEmitting: boolean;
isDispatching: boolean;
isPaused: boolean;
pauseTimer: NodeJS.Timeout | null;
reactChangedStores: Set<FluxStore>;
batched(batch: (...args: any[]) => void): void;
destroy(): void;
emit(): void;
emitNonReactOnce(): void;
emitReactOnce(): void;
getChangeSentinel(): number;
getIsPaused(): boolean;
injectBatchEmitChanges(batch: (...args: any[]) => void): void;
markChanged(store: FluxStore): void;
pause(): void;
resume(): void;
}
export interface Flux { export interface Flux {
Store: typeof FluxStore; Store: typeof FluxStore;
Emitter: FluxEmitter;
} }
export class WindowStore extends FluxStore { export class WindowStore extends FluxStore {

View file

@ -144,6 +144,7 @@ const persistFilter = filters.byCode("[zustand persist middleware]");
export const { persist: zustandPersist } = findLazy(m => m.persist && persistFilter(m.persist)); export const { persist: zustandPersist } = findLazy(m => m.persist && persistFilter(m.persist));
export const MessageActions = findByPropsLazy("editMessage", "sendMessage"); export const MessageActions = findByPropsLazy("editMessage", "sendMessage");
export const MessageCache = findByPropsLazy("clearCache", "_channelMessages");
export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal"); export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal");
export const InviteActions = findByPropsLazy("resolveInvite"); export const InviteActions = findByPropsLazy("resolveInvite");

View file

@ -209,7 +209,7 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
// There are (at the time of writing) 11 modules exporting the window // There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance // Make these non enumerable to improve webpack search performance
if (exports === window && require.c) { if (require.c && (exports === window || exports?.default === window)) {
Object.defineProperty(require.c, id, { Object.defineProperty(require.c, id, {
value: require.c[id], value: require.c[id],
enumerable: false, enumerable: false,
@ -229,7 +229,7 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
for (const [filter, callback] of subscriptions) { for (const [filter, callback] of subscriptions) {
try { try {
if (filter(exports)) { if (exports && filter(exports)) {
subscriptions.delete(filter); subscriptions.delete(filter);
callback(exports, id); callback(exports, id);
} else if (exports.default && filter(exports.default)) { } else if (exports.default && filter(exports.default)) {

View file

@ -106,13 +106,13 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
for (const key in cache) { for (const key in cache) {
const mod = cache[key]; const mod = cache[key];
if (!mod?.exports || mod.exports === window) continue; if (!mod?.exports) continue;
if (filter(mod.exports)) { if (filter(mod.exports)) {
return isWaitFor ? [mod.exports, key] : mod.exports; return isWaitFor ? [mod.exports, key] : mod.exports;
} }
if (mod.exports.default && mod.exports.default !== window && filter(mod.exports.default)) { if (mod.exports.default && filter(mod.exports.default)) {
const found = mod.exports.default; const found = mod.exports.default;
return isWaitFor ? [found, key] : found; return isWaitFor ? [found, key] : found;
} }
@ -264,7 +264,7 @@ export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "f
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
*/ */
export function proxyLazyWebpack<T = any>(factory: () => any, attempts?: number) { export function proxyLazyWebpack<T = any>(factory: () => any, attempts?: number) {
if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
return proxyLazy<T>(factory, attempts); return proxyLazy<T>(factory, attempts);
} }
@ -278,7 +278,7 @@ export function proxyLazyWebpack<T = any>(factory: () => any, attempts?: number)
* @returns Result of factory function * @returns Result of factory function
*/ */
export function LazyComponentWebpack<T extends object = any>(factory: () => any, attempts?: number) { export function LazyComponentWebpack<T extends object = any>(factory: () => any, attempts?: number) {
if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]);
return LazyComponent<T>(factory, attempts); return LazyComponent<T>(factory, attempts);
} }
@ -287,7 +287,7 @@ export function LazyComponentWebpack<T extends object = any>(factory: () => any,
* Find the first module that matches the filter, lazily * Find the first module that matches the filter, lazily
*/ */
export function findLazy(filter: FilterFn) { export function findLazy(filter: FilterFn) {
if (IS_DEV) lazyWebpackSearchHistory.push(["find", [filter]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["find", [filter]]);
return proxyLazy(() => find(filter)); return proxyLazy(() => find(filter));
} }
@ -306,7 +306,7 @@ export function findByProps(...props: string[]) {
* Find the first module that has the specified properties, lazily * Find the first module that has the specified properties, lazily
*/ */
export function findByPropsLazy(...props: string[]) { export function findByPropsLazy(...props: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByProps", props]);
return proxyLazy(() => findByProps(...props)); return proxyLazy(() => findByProps(...props));
} }
@ -325,7 +325,7 @@ export function findByCode(...code: string[]) {
* Find the first function that includes all the given code, lazily * Find the first function that includes all the given code, lazily
*/ */
export function findByCodeLazy(...code: string[]) { export function findByCodeLazy(...code: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByCode", code]);
return proxyLazy(() => findByCode(...code)); return proxyLazy(() => findByCode(...code));
} }
@ -344,7 +344,7 @@ export function findStore(name: string) {
* Find a store by its displayName, lazily * Find a store by its displayName, lazily
*/ */
export function findStoreLazy(name: string) { export function findStoreLazy(name: string) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findStore", [name]]);
return proxyLazy(() => findStore(name)); return proxyLazy(() => findStore(name));
} }
@ -363,7 +363,7 @@ export function findComponentByCode(...code: string[]) {
* Finds the first component that matches the filter, lazily. * Finds the first component that matches the filter, lazily.
*/ */
export function findComponentLazy<T extends object = any>(filter: FilterFn) { export function findComponentLazy<T extends object = any>(filter: FilterFn) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponent", [filter]]);
return LazyComponent<T>(() => { return LazyComponent<T>(() => {
@ -378,7 +378,7 @@ export function findComponentLazy<T extends object = any>(filter: FilterFn) {
* Finds the first component that includes all the given code, lazily * Finds the first component that includes all the given code, lazily
*/ */
export function findComponentByCodeLazy<T extends object = any>(...code: string[]) { export function findComponentByCodeLazy<T extends object = any>(...code: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponentByCode", code]);
return LazyComponent<T>(() => { return LazyComponent<T>(() => {
const res = find(filters.componentByCode(...code), { isIndirect: true }); const res = find(filters.componentByCode(...code), { isIndirect: true });
@ -392,7 +392,7 @@ export function findComponentByCodeLazy<T extends object = any>(...code: string[
* Finds the first component that is exported by the first prop name, lazily * Finds the first component that is exported by the first prop name, lazily
*/ */
export function findExportedComponentLazy<T extends object = any>(...props: string[]) { export function findExportedComponentLazy<T extends object = any>(...props: string[]) {
if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["findExportedComponent", props]);
return LazyComponent<T>(() => { return LazyComponent<T>(() => {
const res = find(filters.byProps(...props), { isIndirect: true }); const res = find(filters.byProps(...props), { isIndirect: true });
@ -402,14 +402,14 @@ export function findExportedComponentLazy<T extends object = any>(...props: stri
}); });
} }
export const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\))|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/; export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/;
export const ChunkIdsRegex = /\("(.+?)"\)/g; export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
/** /**
* Extract and load chunks using their entry point * Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the lazy chunk loading must include * @param code An array of all the code the module factory containing the lazy chunk loading must include
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
* @returns A promise that resolves when the chunks were loaded * @returns A promise that resolves with a boolean whether the chunks were loaded
*/ */
export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) { export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) {
const module = findModuleFactory(...code); const module = findModuleFactory(...code);
@ -417,7 +417,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
const err = new Error("extractAndLoadChunks: Couldn't find module factory"); const err = new Error("extractAndLoadChunks: Couldn't find module factory");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
return; // Strict behaviour in DevBuilds to fail early and make sure the issue is found
if (IS_DEV && !devToolsOpen)
throw err;
return false;
} }
const match = module.toString().match(canonicalizeMatch(matcher)); const match = module.toString().match(canonicalizeMatch(matcher));
@ -429,10 +433,10 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
if (IS_DEV && !devToolsOpen) if (IS_DEV && !devToolsOpen)
throw err; throw err;
return; return false;
} }
const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match; const [, rawChunkIds, entryPointId] = match;
if (Number.isNaN(Number(entryPointId))) { if (Number.isNaN(Number(entryPointId))) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number"); const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
@ -441,16 +445,27 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
if (IS_DEV && !devToolsOpen) if (IS_DEV && !devToolsOpen)
throw err; throw err;
return; return false;
} }
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
if (rawChunkIds) { if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]); const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]);
await Promise.all(chunkIds.map(id => wreq.e(id))); await Promise.all(chunkIds.map(id => wreq.e(id)));
} }
if (wreq.m[entryPointId] == null) {
const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load");
logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
if (IS_DEV && !devToolsOpen)
throw err;
return false;
}
wreq(entryPointId); wreq(entryPointId);
return true;
} }
/** /**
@ -458,11 +473,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
* *
* Extract and load chunks using their entry point * Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the lazy chunk loading must include * @param code An array of all the code the module factory containing the lazy chunk loading must include
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
* @returns A function that returns a promise that resolves when the chunks were loaded, on first call * @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call
*/ */
export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) { export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) {
if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
return makeLazy(() => extractAndLoadChunks(code, matcher)); return makeLazy(() => extractAndLoadChunks(code, matcher));
} }
@ -472,7 +487,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac
* then call the callback with the module as the first argument * then call the callback with the module as the first argument
*/ */
export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
if (IS_DEV && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]);
if (typeof filter === "string") if (typeof filter === "string")
filter = filters.byProps(filter); filter = filters.byProps(filter);