diff --git a/.github/ISSUE_TEMPLATE/blank.yml b/.github/ISSUE_TEMPLATE/blank.yml index e8ca246d..2439d86a 100644 --- a/.github/ISSUE_TEMPLATE/blank.yml +++ b/.github/ISSUE_TEMPLATE/blank.yml @@ -12,7 +12,8 @@ body: DO NOT USE THIS FORM, unless - you are a vencord contributor - you were given explicit permission to use this form by a moderator in our support server - - you are filing a security related report + + DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new) - type: textarea id: content diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7a2f24e..ba22b123 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build web - run: pnpm buildWeb --standalone + run: pnpm buildWebStandalone - name: Build run: pnpm build --standalone diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8407e08e..190e3069 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build web - run: pnpm buildWeb --standalone + run: pnpm buildWebStandalone - name: Publish extension run: | diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index d3a882fa..a669c1a2 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -37,8 +37,8 @@ jobs: with: chrome-version: stable - - name: Build web - run: pnpm buildWeb --standalone --dev + - name: Build Vencord Reporter Version + run: pnpm buildReporter - name: Create Report timeout-minutes: 10 diff --git a/package.json b/package.json index 29b1506e..01fe3552 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.8.6", + "version": "1.8.8", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { @@ -20,7 +20,11 @@ "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "buildStandalone": "pnpm build --standalone", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", + "buildWebStandalone": "pnpm buildWeb --standalone", + "buildReporter": "pnpm buildWebStandalone --reporter --skip-extension", + "buildReporterDesktop": "pnpm build --reporter", "watch": "pnpm build --watch", + "watchWeb": "pnpm buildWeb --watch", "generatePluginJson": "tsx scripts/generatePluginList.ts", "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types", "inject": "node scripts/runInstaller.mjs", @@ -103,6 +107,6 @@ }, "engines": { "node": ">=18", - "pnpm": ">=8" + "pnpm": ">=9" } } diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 0c2a930a..fcf56f66 100755 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -21,19 +21,21 @@ import esbuild from "esbuild"; import { readdir } from "fs/promises"; 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 = { - IS_STANDALONE: isStandalone, - IS_DEV: JSON.stringify(isDev), - IS_UPDATER_DISABLED: updaterDisabled, + IS_STANDALONE, + IS_DEV, + IS_REPORTER, + IS_UPDATER_DISABLED, IS_WEB: false, IS_EXTENSION: false, 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 defines["process.platform"] = JSON.stringify(process.platform); @@ -46,7 +48,7 @@ const nodeCommonOpts = { platform: "node", target: ["esnext"], external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external], - define: defines, + define: defines }; const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`; @@ -73,13 +75,13 @@ const globNativesPlugin = { let i = 0; for (const dir of pluginDirs) { const dirPath = join("src", dir); - if (!await existsAsync(dirPath)) continue; + if (!await exists(dirPath)) continue; const plugins = await readdir(dirPath); for (const p of plugins) { const nativePath = join(dirPath, p, "native.ts"); const indexNativePath = join(dirPath, p, "native/index.ts"); - if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath))) + if (!(await exists(nativePath)) && !(await exists(indexNativePath))) continue; const nameParts = p.split("."); diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index b4c72606..bc15ccce 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises import { join } from "path"; 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} @@ -33,22 +33,23 @@ const commonOptions = { entryPoints: ["browser/Vencord.ts"], globalName: "Vencord", format: "iife", - external: ["plugins", "git-hash", "/assets/*"], + external: ["~plugins", "~git-hash", "/assets/*"], plugins: [ globPlugins("web"), ...commonOpts.plugins, ], target: ["esnext"], define: { - IS_WEB: "true", - IS_EXTENSION: "false", - IS_STANDALONE: "true", - IS_DEV: JSON.stringify(isDev), - IS_DISCORD_DESKTOP: "false", - IS_VESKTOP: "false", - IS_UPDATER_DISABLED: "true", + IS_WEB: true, + IS_EXTENSION: false, + IS_STANDALONE: true, + IS_DEV, + IS_REPORTER, + IS_DISCORD_DESKTOP: false, + IS_VESKTOP: false, + IS_UPDATER_DISABLED: true, VERSION: JSON.stringify(VERSION), - BUILD_TIMESTAMP, + BUILD_TIMESTAMP } }; @@ -87,16 +88,16 @@ await Promise.all( esbuild.build({ ...commonOptions, outfile: "dist/browser.js", - footer: { js: "//# sourceURL=VencordWeb" }, + footer: { js: "//# sourceURL=VencordWeb" } }), esbuild.build({ ...commonOptions, outfile: "dist/extension.js", define: { ...commonOptions?.define, - IS_EXTENSION: "true", + IS_EXTENSION: true, }, - footer: { js: "//# sourceURL=VencordWeb" }, + footer: { js: "//# sourceURL=VencordWeb" } }), esbuild.build({ ...commonOptions, @@ -112,7 +113,7 @@ await Promise.all( footer: { // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});" - }, + } }) ] ); @@ -165,7 +166,7 @@ async function buildExtension(target, files) { f.startsWith("manifest") ? "manifest.json" : f, content ]; - }))), + }))) }; 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); }); -await Promise.all([ - appendCssRuntime, - buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]), - buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]), -]); +if (!process.argv.includes("--skip-extension")) { + await Promise.all([ + appendCssRuntime, + buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.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"); -console.info("Packed Chromium Extension written to 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"); -Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip"); -console.info("Packed Firefox Extension written to 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"); + +} else { + await appendCssRuntime; +} diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 3b1473e1..cdbb26ee 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -35,24 +35,26 @@ const PackageJSON = JSON.parse(readFileSync("package.json")); export const VERSION = PackageJSON.version; // https://reproducible-builds.org/docs/source-date-epoch/ export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now(); + export const watch = process.argv.includes("--watch"); -export const isDev = watch || process.argv.includes("--dev"); -export const isStandalone = JSON.stringify(process.argv.includes("--standalone")); -export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater")); +export const IS_DEV = watch || process.argv.includes("--dev"); +export const IS_REPORTER = process.argv.includes("--reporter"); +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 banner = { js: ` // Vencord ${gitHash} -// Standalone: ${isStandalone} -// Platform: ${isStandalone === "false" ? process.platform : "Universal"} -// Updater disabled: ${updaterDisabled} +// Standalone: ${IS_STANDALONE} +// Platform: ${IS_STANDALONE === false ? process.platform : "Universal"} +// Updater Disabled: ${IS_UPDATER_DISABLED} `.trim() }; -const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs")); - -export function existsAsync(path) { - return access(path, FsConstants.F_OK) +export async function exists(path) { + return await access(path, FsConstants.F_OK) .then(() => true) .catch(() => false); } @@ -66,7 +68,7 @@ export const makeAllPackagesExternalPlugin = { setup(build) { const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../" build.onResolve({ filter }, args => ({ path: args.path, external: true })); - }, + } }; /** @@ -89,14 +91,14 @@ export const globPlugins = kind => ({ let plugins = "\n"; let i = 0; for (const dir of pluginDirs) { - if (!await existsAsync(`./src/${dir}`)) continue; + if (!await exists(`./src/${dir}`)) continue; const files = await readdir(`./src/${dir}`); for (const file of files) { if (file.startsWith("_") || file.startsWith(".")) continue; if (file === "index.ts") continue; const target = getPluginTarget(file); - if (target) { + if (target && !IS_REPORTER) { if (target === "dev" && !watch) continue; if (target === "web" && kind === "discordDesktop") continue; if (target === "desktop" && kind === "web") continue; @@ -178,7 +180,7 @@ export const fileUrlPlugin = { build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => { const { searchParams } = new URL(uri); 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 encoding = base64 ? "base64" : "utf-8"; diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 8bb87d81..0fde4863 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +/* eslint-disable no-fallthrough */ + // eslint-disable-next-line spaced-comment /// // eslint-disable-next-line spaced-comment @@ -40,10 +42,11 @@ const browser = await pup.launch({ const page = await browser.newPage(); await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); +await page.setBypassCSP(true); -function maybeGetError(handle: JSHandle) { - return (handle as JSHandle)?.getProperty("message") - .then(m => m.jsonValue()); +async function maybeGetError(handle: JSHandle): Promise { + return await (handle as JSHandle)?.getProperty("message") + .then(m => m?.jsonValue()); } const report = { @@ -59,6 +62,7 @@ const report = { error: string; }[], otherErrors: [] as string[], + ignoredErrors: [] as string[], badWebpackFinds: [] as string[] }; @@ -106,15 +110,6 @@ async function printReport() { console.log(); - const ignoredErrors = [] as string[]; - report.otherErrors = report.otherErrors.filter(e => { - if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) { - ignoredErrors.push(e); - return false; - } - return true; - }); - console.log("## Discord Errors"); report.otherErrors.forEach(e => { console.log(`- ${toCodeBlock(e)}`); @@ -123,7 +118,7 @@ async function printReport() { console.log(); console.log("## Ignored Discord Errors"); - ignoredErrors.forEach(e => { + report.ignoredErrors.forEach(e => { console.log(`- ${toCodeBlock(e)}`); }); @@ -188,61 +183,6 @@ page.on("console", async e => { const level = e.type(); const rawArgs = e.args(); - const firstArg = await rawArgs[0]?.jsonValue(); - if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") { - await browser.close(); - await printReport(); - process.exit(); - } - - const isVencord = firstArg === "[Vencord]"; - const isDebug = firstArg === "[PUP_DEBUG]"; - const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]"; - - if (isWebpackFindFail) { - process.exitCode = 1; - report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string); - } - - if (isVencord) { - const args = await Promise.all(e.args().map(a => a.jsonValue())); - - const [, tag, message] = args as Array; - const cause = await maybeGetError(e.args()[3]); - - switch (tag) { - case "WebpackInterceptor:": - const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; - if (!patchFailMatch) break; - - process.exitCode = 1; - - const [, plugin, type, id, regex] = patchFailMatch; - report.badPatches.push({ - plugin, - type, - id, - match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"), - error: cause - }); - - break; - case "PluginManager:": - const failedToStartMatch = message.match(/Failed to start (.+)/); - if (!failedToStartMatch) break; - - process.exitCode = 1; - - const [, name] = failedToStartMatch; - report.badStarts.push({ - plugin: name, - error: cause - }); - - break; - } - } - async function getText() { try { return await Promise.all( @@ -255,299 +195,105 @@ page.on("console", async e => { } } - if (isDebug) { - const text = await getText(); + const firstArg = await rawArgs[0]?.jsonValue(); - console.error(text); - if (text.includes("A fatal error occurred:")) { - process.exit(1); + const isVencord = firstArg === "[Vencord]"; + const isDebug = firstArg === "[PUP_DEBUG]"; + + outer: + if (isVencord) { + try { + var args = await Promise.all(e.args().map(a => a.jsonValue())); + } catch { + break outer; } + + const [, tag, message, otherMessage] = args as Array; + + switch (tag) { + case "WebpackInterceptor:": + const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; + if (!patchFailMatch) break; + + console.error(await getText()); + process.exitCode = 1; + + const [, plugin, type, id, regex] = patchFailMatch; + report.badPatches.push({ + plugin, + type, + id, + match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"), + error: await maybeGetError(e.args()[3]) + }); + + break; + case "PluginManager:": + const failedToStartMatch = message.match(/Failed to start (.+)/); + if (!failedToStartMatch) break; + + console.error(await getText()); + process.exitCode = 1; + + const [, name] = failedToStartMatch; + report.badStarts.push({ + plugin: name, + error: await maybeGetError(e.args()[3]) ?? "Unknown error" + }); + + break; + case "Reporter:": + console.error(await getText()); + + switch (message) { + case "Webpack Find Fail:": + process.exitCode = 1; + report.badWebpackFinds.push(otherMessage); + break; + case "A fatal error occurred:": + process.exit(1); + case "Finished test": + await browser.close(); + await printReport(); + process.exit(); + } + } + } + + if (isDebug) { + console.error(await getText()); } else if (level === "error") { const text = await getText(); if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) { - console.error("[Unexpected Error]", text); - report.otherErrors.push(text); + if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) { + report.ignoredErrors.push(text); + } else { + console.error("[Unexpected Error]", text); + report.otherErrors.push(text); + } } } }); -page.on("error", e => console.error("[Error]", e)); -page.on("pageerror", e => console.error("[Page Error]", e)); +page.on("error", e => console.error("[Error]", e.message)); +page.on("pageerror", e => console.error("[Page Error]", e.message)); -await page.setBypassCSP(true); - -async function runtime(token: string) { - console.log("[PUP_DEBUG]", "Starting test..."); - - try { - // Spoof languages to not be suspicious - Object.defineProperty(navigator, "languages", { - get: function () { - 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; - - const { canonicalizeMatch, Logger } = Vencord.Util; - - const validChunks = new Set(); - const invalidChunks = new Set(); - const deferredRequires = new Set(); - - let chunksSearchingResolve: (value: void | PromiseLike) => void; - const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); - - // True if resolved, false otherwise - const chunksSearchPromises = [] as Array<() => boolean>; - - const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); - - async function searchAndLoadLazyChunks(factoryCode: string) { - const lazyChunks = factoryCode.matchAll(LazyChunkRegex); - const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); - - // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before - // the chunk containing the component - const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); - - await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => { - const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle; - const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : []; - - if (chunkIds.length === 0) { - return; - } - - let invalidChunkGroup = false; - - for (const id of chunkIds) { - if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; - - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - if (isWasm) { - invalidChunks.add(id); - invalidChunkGroup = true; - continue; - } - - validChunks.add(id); - } - - if (!invalidChunkGroup) { - validChunkGroups.add([chunkIds, entryPoint]); - } - })); - - // Loads all found valid chunk groups - await Promise.all( - Array.from(validChunkGroups) - .map(([chunkIds]) => - Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) - ) - ); - - // Requires the entry points for all valid chunk groups - for (const [, entryPoint] of validChunkGroups) { - try { - if (shouldForceDefer) { - deferredRequires.add(entryPoint); - continue; - } - - if (wreq.m[entryPoint]) wreq(entryPoint as any); - } catch (err) { - console.error(err); - } - } - - // setImmediate to only check if all chunks were loaded after this function resolves - // We check if all chunks were loaded every time a factory is loaded - // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved - // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them - setTimeout(() => { - let allResolved = true; - - for (let i = 0; i < chunksSearchPromises.length; i++) { - const isResolved = chunksSearchPromises[i](); - - if (isResolved) { - // Remove finished promises to avoid having to iterate through a huge array everytime - chunksSearchPromises.splice(i--, 1); - } else { - allResolved = false; - } - } - - if (allResolved) chunksSearchingResolve(); - }, 0); +async function reporterRuntime(token: string) { + Vencord.Webpack.waitFor( + "loginToken", + m => { + console.log("[PUP_DEBUG]", "Logging in with token..."); + m.loginToken(token); } - - Vencord.Webpack.waitFor( - "loginToken", - m => { - console.log("[PUP_DEBUG]", "Logging in with token..."); - m.loginToken(token); - } - ); - - Vencord.Webpack.beforeInitListeners.add(async webpackRequire => { - console.log("[PUP_DEBUG]", "Loading all chunks..."); - - wreq = webpackRequire; - - Vencord.Webpack.factoryListeners.add(factory => { - let isResolved = false; - searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - }); - - // setImmediate to only search the initial factories after Discord initialized the app - // our beforeInitListeners are called before Discord initializes the app - setTimeout(() => { - for (const factoryId in wreq.m) { - let isResolved = false; - searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - } - }, 0); - }); - - await chunksSearchingDone; - - // Require deferred entry points - for (const deferredRequire of deferredRequires) { - wreq!(deferredRequire as any); - } - - // All chunks Discord has mapped to asset files, even if they are not used anymore - const allChunks = [] as string[]; - - // Matches "id" or id: - for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { - const id = currentMatch[1] ?? currentMatch[2]; - if (id == null) continue; - - allChunks.push(id); - } - - if (allChunks.length === 0) throw new Error("Failed to get all chunks"); - - // Chunks that are not loaded (not used) by Discord code anymore - const chunksLeft = allChunks.filter(id => { - return !(validChunks.has(id) || invalidChunks.has(id)); - }); - - await Promise.all(chunksLeft.map(async id => { - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - // Loads and requires a chunk - if (!isWasm) { - await wreq.e(id as any); - if (wreq.m[id]) wreq(id as any); - } - })); - - console.log("[PUP_DEBUG]", "Finished loading all chunks!"); - - for (const patch of Vencord.Plugins.patches) { - if (!patch.all) { - new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); - } - } - - for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) { - let method = searchType; - - if (searchType === "findComponent") method = "find"; - if (searchType === "findExportedComponent") method = "findByProps"; - if (searchType === "waitFor" || searchType === "waitForComponent") { - if (typeof args[0] === "string") method = "findByProps"; - else method = "find"; - } - if (searchType === "waitForStore") method = "findStore"; - - try { - let result: any; - - if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { - const [factory] = args; - result = factory(); - } else if (method === "extractAndLoadChunks") { - const [code, matcher] = args; - - const module = Vencord.Webpack.findModuleFactory(...code); - if (module) result = module.toString().match(canonicalizeMatch(matcher)); - } else { - // @ts-ignore - result = Vencord.Webpack[method](...args); - } - - if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; - } catch (e) { - let logMessage = searchType; - if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; - else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; - else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; - - console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage); - } - } - - setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); - } catch (e) { - console.log("[PUP_DEBUG]", "A fatal error occurred:", e); - } + ); } await page.evaluateOnNewDocument(` - ${readFileSync("./dist/browser.js", "utf-8")} - - ;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); + if (location.host.endsWith("discord.com")) { + ${readFileSync("./dist/browser.js", "utf-8")}; + (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); + } `); await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login"); diff --git a/src/Vencord.ts b/src/Vencord.ts index 72541148..c4c6d470 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -42,6 +42,10 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater"; import { onceReady } from "./webpack"; import { SettingsRouter } from "./webpack/common"; +if (IS_REPORTER) { + require("./debug/runReporter"); +} + async function syncSettings() { // pre-check for local shared settings if ( diff --git a/src/api/DataStore/index.ts b/src/api/DataStore/index.ts index 97f43edd..47ae39db 100644 --- a/src/api/DataStore/index.ts +++ b/src/api/DataStore/index.ts @@ -49,7 +49,7 @@ let defaultGetStoreFunc: UseStore | undefined; function defaultGetStore() { if (!defaultGetStoreFunc) { - defaultGetStoreFunc = createStore("VencordData", "VencordStore"); + defaultGetStoreFunc = createStore(!IS_REPORTER ? "VencordData" : "VencordDataReporter", "VencordStore"); } return defaultGetStoreFunc; } diff --git a/src/api/MessageUpdater.ts b/src/api/MessageUpdater.ts new file mode 100644 index 00000000..5cac8052 --- /dev/null +++ b/src/api/MessageUpdater.ts @@ -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) { + 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(); +} diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 490e6ef7..b94e6a3f 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -106,7 +106,7 @@ const DefaultSettings: Settings = { } }; -const settings = VencordNative.settings.get(); +const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings; mergeDefaults(settings, DefaultSettings); const saveSettingsOnFrequentAction = debounce(async () => { @@ -156,12 +156,14 @@ export const SettingsStore = new SettingsStoreClass(settings, { } }); -SettingsStore.addGlobalChangeListener((_, path) => { - SettingsStore.plain.cloud.settingsSyncVersion = Date.now(); - localStorage.Vencord_settingsDirty = true; - saveSettingsOnFrequentAction(); - VencordNative.settings.set(SettingsStore.plain, path); -}); +if (!IS_REPORTER) { + SettingsStore.addGlobalChangeListener((_, path) => { + SettingsStore.plain.cloud.settingsSyncVersion = Date.now(); + localStorage.Vencord_settingsDirty = true; + saveSettingsOnFrequentAction(); + VencordNative.settings.set(SettingsStore.plain, path); + }); +} /** * Same as {@link Settings} but unproxied. You should treat this as readonly, diff --git a/src/api/index.ts b/src/api/index.ts index 5dca6310..02c70008 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -26,6 +26,7 @@ import * as $MessageAccessories from "./MessageAccessories"; import * as $MessageDecorations from "./MessageDecorations"; import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessagePopover from "./MessagePopover"; +import * as $MessageUpdater from "./MessageUpdater"; import * as $Notices from "./Notices"; import * as $Notifications from "./Notifications"; import * as $ServerList from "./ServerList"; @@ -110,3 +111,8 @@ export const ContextMenu = $ContextMenu; * An API allowing you to add buttons to the chat input */ export const ChatButtons = $ChatButtons; + +/** + * An API allowing you to update and re-render messages + */ +export const MessageUpdater = $MessageUpdater; diff --git a/src/debug/Tracer.ts b/src/debug/Tracer.ts index 4337e001..7d80f425 100644 --- a/src/debug/Tracer.ts +++ b/src/debug/Tracer.ts @@ -18,14 +18,14 @@ import { Logger } from "@utils/Logger"; -if (IS_DEV) { +if (IS_DEV || IS_REPORTER) { var traces = {} as Record; var logger = new Logger("Tracer", "#FFD166"); } const noop = function () { }; -export const beginTrace = !IS_DEV ? noop : +export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop : function beginTrace(name: string, ...args: any[]) { if (name in traces) throw new Error(`Trace ${name} already exists!`); @@ -33,7 +33,7 @@ export const beginTrace = !IS_DEV ? noop : 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 [start, args] = traces[name]; @@ -48,7 +48,7 @@ type TraceNameMapper = (...args: Parameters) => string; const noopTracer = (name: string, f: F, mapper?: TraceNameMapper) => f; -export const traceFunction = !IS_DEV +export const traceFunction = !(IS_DEV || IS_REPORTER) ? noopTracer : function traceFunction(name: string, f: F, mapper?: TraceNameMapper): F { return function (this: any, ...args: Parameters) { diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts new file mode 100644 index 00000000..61c9f162 --- /dev/null +++ b/src/debug/runReporter.ts @@ -0,0 +1,224 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; +import { canonicalizeMatch } from "@utils/patches"; +import * as Webpack from "@webpack"; +import { wreq } from "@webpack"; +import { patches } from "plugins"; + +const ReporterLogger = new Logger("Reporter"); + +async function runReporter() { + ReporterLogger.log("Starting test..."); + + try { + const validChunks = new Set(); + const invalidChunks = new Set(); + const deferredRequires = new Set(); + + let chunksSearchingResolve: (value: void | PromiseLike) => void; + const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); + + // True if resolved, false otherwise + const chunksSearchPromises = [] as Array<() => boolean>; + + const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); + + async function searchAndLoadLazyChunks(factoryCode: string) { + const lazyChunks = factoryCode.matchAll(LazyChunkRegex); + const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); + + // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before + // the chunk containing the component + const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); + + await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { + const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; + + if (chunkIds.length === 0) { + return; + } + + let invalidChunkGroup = false; + + for (const id of chunkIds) { + if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; + + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + if (isWasm && IS_WEB) { + invalidChunks.add(id); + invalidChunkGroup = true; + continue; + } + + validChunks.add(id); + } + + if (!invalidChunkGroup) { + validChunkGroups.add([chunkIds, entryPoint]); + } + })); + + // Loads all found valid chunk groups + await Promise.all( + Array.from(validChunkGroups) + .map(([chunkIds]) => + Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) + ) + ); + + // Requires the entry points for all valid chunk groups + for (const [, entryPoint] of validChunkGroups) { + try { + if (shouldForceDefer) { + deferredRequires.add(entryPoint); + continue; + } + + if (wreq.m[entryPoint]) wreq(entryPoint as any); + } catch (err) { + console.error(err); + } + } + + // setImmediate to only check if all chunks were loaded after this function resolves + // We check if all chunks were loaded every time a factory is loaded + // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved + // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them + setTimeout(() => { + let allResolved = true; + + for (let i = 0; i < chunksSearchPromises.length; i++) { + const isResolved = chunksSearchPromises[i](); + + if (isResolved) { + // Remove finished promises to avoid having to iterate through a huge array everytime + chunksSearchPromises.splice(i--, 1); + } else { + allResolved = false; + } + } + + if (allResolved) chunksSearchingResolve(); + }, 0); + } + + Webpack.beforeInitListeners.add(async () => { + ReporterLogger.log("Loading all chunks..."); + + Webpack.factoryListeners.add(factory => { + let isResolved = false; + searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + }); + + // setImmediate to only search the initial factories after Discord initialized the app + // our beforeInitListeners are called before Discord initializes the app + setTimeout(() => { + for (const factoryId in wreq.m) { + let isResolved = false; + searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + } + }, 0); + }); + + await chunksSearchingDone; + + // Require deferred entry points + for (const deferredRequire of deferredRequires) { + wreq!(deferredRequire as any); + } + + // All chunks Discord has mapped to asset files, even if they are not used anymore + const allChunks = [] as string[]; + + // Matches "id" or id: + for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + const id = currentMatch[1] ?? currentMatch[2]; + if (id == null) continue; + + allChunks.push(id); + } + + if (allChunks.length === 0) throw new Error("Failed to get all chunks"); + + // Chunks that are not loaded (not used) by Discord code anymore + const chunksLeft = allChunks.filter(id => { + return !(validChunks.has(id) || invalidChunks.has(id)); + }); + + await Promise.all(chunksLeft.map(async id => { + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + // Loads and requires a chunk + if (!isWasm) { + await wreq.e(id as any); + if (wreq.m[id]) wreq(id as any); + } + })); + + ReporterLogger.log("Finished loading all chunks!"); + + for (const patch of patches) { + if (!patch.all) { + new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); + } + } + + for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) { + let method = searchType; + + if (searchType === "findComponent") method = "find"; + if (searchType === "findExportedComponent") method = "findByProps"; + if (searchType === "waitFor" || searchType === "waitForComponent") { + if (typeof args[0] === "string") method = "findByProps"; + else method = "find"; + } + if (searchType === "waitForStore") method = "findStore"; + + try { + let result: any; + + if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { + const [factory] = args; + result = factory(); + } else if (method === "extractAndLoadChunks") { + const [code, matcher] = args; + + result = await Webpack.extractAndLoadChunks(code, matcher); + if (result === false) result = null; + } else { + // @ts-ignore + result = Webpack[method](...args); + } + + if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; + } catch (e) { + let logMessage = searchType; + if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; + else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; + else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; + + ReporterLogger.log("Webpack Find Fail:", logMessage); + } + } + + ReporterLogger.log("Finished test"); + } catch (e) { + ReporterLogger.log("A fatal error occurred:", e); + } +} + +runReporter(); diff --git a/src/globals.d.ts b/src/globals.d.ts index 94b5f15e..e20ca4b7 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -34,9 +34,10 @@ declare global { */ export var IS_WEB: boolean; export var IS_EXTENSION: boolean; - export var IS_DEV: boolean; export var IS_STANDALONE: 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_VESKTOP: boolean; export var VERSION: string; diff --git a/src/main/updater/index.ts b/src/main/updater/index.ts index 32d5cd66..539b02a4 100644 --- a/src/main/updater/index.ts +++ b/src/main/updater/index.ts @@ -17,4 +17,4 @@ */ if (!IS_UPDATER_DISABLED) - import(IS_STANDALONE ? "./http" : "./git"); + require(IS_STANDALONE ? "./http" : "./git"); diff --git a/src/plugins/_api/messageUpdater.ts b/src/plugins/_api/messageUpdater.ts new file mode 100644 index 00000000..8f6cca26 --- /dev/null +++ b/src/plugins/_api/messageUpdater.ts @@ -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 . +*/ + +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: "" + } + } + ] +}); diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index c7377833..d59d82af 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -65,7 +65,7 @@ export default definePlugin({ commands: [{ name: "vencord-debug", description: "Send Vencord Debug info", - predicate: ctx => AllowedChannelIds.includes(ctx.channel.id), + predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id), async execute() { const { RELEASE_CHANNEL } = window.GLOBAL_ENV; diff --git a/src/plugins/arRPC.web/index.tsx b/src/plugins/arRPC.web/index.tsx index 423dce9b..e41e8675 100644 --- a/src/plugins/arRPC.web/index.tsx +++ b/src/plugins/arRPC.web/index.tsx @@ -19,7 +19,7 @@ import { popNotice, showNotice } from "@api/Notices"; import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { ReporterTestable } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common"; @@ -41,6 +41,7 @@ export default definePlugin({ name: "WebRichPresence (arRPC)", description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)", authors: [Devs.Ducko], + reporterTestable: ReporterTestable.None, settingsAboutComponent: () => ( <> diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index 25fd563e..a495907b 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; 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"; const PORT = 8485; @@ -243,6 +243,7 @@ export default definePlugin({ name: "DevCompanion", description: "Dev Companion Plugin", authors: [Devs.Ven], + reporterTestable: ReporterTestable.None, settings, toolboxActions: { diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 427f36ce..737406cf 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -333,7 +333,7 @@ export default definePlugin({ ] }, { - find: "renderEmbeds(", + find: "}renderEmbeds(", replacement: [ { // Call our function to decide whether the embed should be ignored or not diff --git a/src/plugins/friendsSince/index.tsx b/src/plugins/friendsSince/index.tsx index eb59fff2..58014f36 100644 --- a/src/plugins/friendsSince/index.tsx +++ b/src/plugins/friendsSince/index.tsx @@ -36,7 +36,7 @@ export default definePlugin({ { find: ".UserPopoutUpsellSource.PROFILE_PANEL,", replacement: { - match: /\i.default,\{userId:(\i)}\)/, + match: /\i.default,\{userId:([^,]+?)}\)/, replace: "$&,$self.friendsSince({ userId: $1 })" } }, diff --git a/src/plugins/index.ts b/src/plugins/index.ts index a434b4a6..53ab7983 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -21,7 +21,7 @@ import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; 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 { FluxEvents } from "@webpack/types"; @@ -39,35 +39,68 @@ export const patches = [] as Patch[]; let enabledPluginsSubscribedFlux = false; const subscribedFluxEventsPlugins = new Set(); +const pluginsValues = Object.values(Plugins); const settings = Settings.plugins; export function isPluginEnabled(p: string) { return ( + IS_REPORTER || Plugins[p]?.required || Plugins[p]?.isDependency || settings[p]?.enabled ) ?? false; } -const pluginsValues = Object.values(Plugins); +export function addPatch(newPatch: Omit, 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 // 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 => { const dep = Plugins[d]; - if (dep) { - settings[d].enabled = true; - dep.isDependency = true; - } - else { + + if (!dep) { const error = new Error(`Plugin ${p.name} has unresolved dependency ${d}`); - if (IS_DEV) + + if (IS_DEV) { throw error; + } + logger.warn(error); + return; } + + settings[d].enabled = true; + dep.isDependency = true; }); } @@ -82,23 +115,18 @@ for (const p of pluginsValues) { } if (p.patches && isPluginEnabled(p.name)) { - for (const patch of p.patches) { - patch.plugin = p.name; - - canonicalizeFind(patch); - if (!Array.isArray(patch.replacement)) { - patch.replacement = [patch.replacement]; + if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) { + for (const patch of p.patches) { + addPatch(patch, p.name); } - - patches.push(patch); } } } export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins(target: StartAt) { logger.info(`Starting plugins (stage ${target})`); - for (const name in Plugins) - if (isPluginEnabled(name)) { + for (const name in Plugins) { + if (isPluginEnabled(name) && (!IS_REPORTER || isReporterTestable(Plugins[name], ReporterTestable.Start))) { const p = Plugins[name]; const startAt = p.startAt ?? StartAt.WebpackReady; @@ -106,30 +134,38 @@ export const startAllPlugins = traceFunction("startAllPlugins", function startAl startPlugin(Plugins[name]); } + } }); export function startDependenciesRecursive(p: Plugin) { let restartNeeded = false; const failures: string[] = []; - p.dependencies?.forEach(dep => { - if (!Settings.plugins[dep].enabled) { - startDependenciesRecursive(Plugins[dep]); + + p.dependencies?.forEach(d => { + if (!settings[d].enabled) { + const dep = Plugins[d]; + startDependenciesRecursive(dep); + // If the plugin has patches, don't start the plugin, just enable it. - Settings.plugins[dep].enabled = true; - if (Plugins[dep].patches) { - logger.warn(`Enabling dependency ${dep} requires restart.`); + settings[d].enabled = true; + dep.isDependency = true; + + if (dep.patches) { + logger.warn(`Enabling dependency ${d} requires restart.`); restartNeeded = true; return; } - const result = startPlugin(Plugins[dep]); - if (!result) failures.push(dep); + + const result = startPlugin(dep); + if (!result) failures.push(d); } }); + return { restartNeeded, failures }; } 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); logger.debug("Subscribing to flux events of plugin", p.name); diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index 3dfe51e7..01199d99 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -18,12 +18,13 @@ import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addButton, removeButton } from "@api/MessagePopover"; +import { updateMessage } from "@api/MessageUpdater"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; -import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; +import { ChannelStore, Constants, RestAPI, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { buildDecModal } from "./components/DecryptionModal"; @@ -103,7 +104,10 @@ export default definePlugin({ name: "InvisibleChat", description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], - dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"], + dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"], + reporterTestable: ReporterTestable.Patches, + settings, + patches: [ { // Indicator @@ -120,7 +124,6 @@ export default definePlugin({ URL_REGEX: new RegExp( /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, ), - settings, async start() { addButton("InvisibleChat", message => { return this.INV_REGEX.test(message?.content) @@ -180,14 +183,7 @@ export default definePlugin({ message.embeds.push(embed); } - this.updateMessage(message); - }, - - updateMessage: (message: any) => { - FluxDispatcher.dispatch({ - type: "MESSAGE_UPDATE", - message, - }); + updateMessage(message.channel_id, message.id, { embeds: message.embeds }); }, popOverIcon: () => , diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 6c8fd83e..e76d53e4 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -17,6 +17,7 @@ */ import { addAccessory, removeAccessory } from "@api/MessageAccessories"; +import { updateMessage } from "@api/MessageUpdater"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants.js"; @@ -28,7 +29,6 @@ import { Button, ChannelStore, Constants, - FluxDispatcher, GuildStore, IconUtils, MessageStore, @@ -250,15 +250,9 @@ function MessageEmbedAccessory({ message }: { message: Message; }) { if (linkedMessage) { messageCache.set(messageID, { message: linkedMessage, fetched: true }); } else { - const msg = { ...message } as any; - delete msg.embeds; - delete msg.interaction; messageFetchQueue.unshift(() => fetchMessage(channelID, messageID) - .then(m => m && FluxDispatcher.dispatch({ - type: "MESSAGE_UPDATE", - message: msg - })) + .then(m => m && updateMessage(message.channel_id, message.id)) ); continue; } @@ -367,7 +361,7 @@ export default definePlugin({ name: "MessageLinkEmbeds", description: "Adds a preview to messages that link another message", authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev], - dependencies: ["MessageAccessoriesAPI"], + dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI"], settings, diff --git a/src/plugins/secretRingTone/index.ts b/src/plugins/secretRingTone/index.ts index 9c3956a8..be804efc 100644 --- a/src/plugins/secretRingTone/index.ts +++ b/src/plugins/secretRingTone/index.ts @@ -16,9 +16,8 @@ export default definePlugin({ { find: '"call_ringing_beat"', replacement: { - // FIXME Remove === alternative when it hits stable - match: /500(!==|===)\i\(\)\.random\(1,1e3\)/, - replace: (_, predicate) => predicate === "!==" ? "false" : "true", + match: /500!==\i\(\)\.random\(1,1e3\)/, + replace: "false", } }, ], diff --git a/src/plugins/shikiCodeblocks.desktop/index.ts b/src/plugins/shikiCodeblocks.desktop/index.ts index 27463195..e6676a86 100644 --- a/src/plugins/shikiCodeblocks.desktop/index.ts +++ b/src/plugins/shikiCodeblocks.desktop/index.ts @@ -20,7 +20,7 @@ import "./shiki.css"; import { enableStyle } from "@api/Styles"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { ReporterTestable } from "@utils/types"; import previewExampleText from "file://previewExample.tsx"; import { shiki } from "./api/shiki"; @@ -34,6 +34,9 @@ export default definePlugin({ name: "ShikiCodeblocks", description: "Brings vscode-style codeblocks into Discord, powered by Shiki", authors: [Devs.Vap], + reporterTestable: ReporterTestable.Patches, + settings, + patches: [ { find: "codeBlock:{react(", @@ -66,7 +69,6 @@ export default definePlugin({ isPreview: true, tempSettings, }), - settings, // exports shiki, diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx index d70c0931..a78e4c41 100644 --- a/src/plugins/showConnections/index.tsx +++ b/src/plugins/showConnections/index.tsx @@ -182,7 +182,7 @@ export default definePlugin({ } }, { - find: "\"Profile Panel: user cannot be undefined\"", + find: ".UserPopoutUpsellSource.PROFILE_PANEL,", replacement: { // createElement(Divider, {}), createElement(NoteComponent) match: /\(0,\i\.jsx\)\(\i\.\i,\{\}\).{0,100}setNote:(?=.+?channelId:(\i).id)/, diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index c120d72d..35d56091 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -73,8 +73,9 @@ export default definePlugin({ find: '"placeholder-channel-id"', replacement: [ // Remove the special logic for channels we don't have access to + // FIXME Remove variable matcher from threadsIds when it hits stable { - match: /if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{if\(this\.id===\i\).+?threadIds:\i}}/, + match: /if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{if\(this\.id===\i\).+?threadIds:(?:\[\]|\i)}}/, replace: "" }, // Do not check for unreads when selecting the render level if the channel is hidden diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx index ac629e74..6e8e4bbf 100644 --- a/src/plugins/vcNarrator/index.tsx +++ b/src/plugins/vcNarrator/index.tsx @@ -22,7 +22,7 @@ import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; 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 { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common"; @@ -155,6 +155,7 @@ export default definePlugin({ name: "VcNarrator", description: "Announces when users join, leave, or move voice channels via narrator", authors: [Devs.Ven], + reporterTestable: ReporterTestable.None, flux: { VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts index 5251959f..caa44a40 100644 --- a/src/plugins/xsOverlay.desktop/index.ts +++ b/src/plugins/xsOverlay.desktop/index.ts @@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; 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 { ChannelStore, GuildStore, UserStore } from "@webpack/common"; 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", authors: [Devs.Nyako], tags: ["vr", "notify"], + reporterTestable: ReporterTestable.None, settings, + flux: { CALL_UPDATE({ call }: { call: Call; }) { if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) { diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 1ae4762d..5296184d 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -32,6 +32,11 @@ export class Logger { constructor(public name: string, public color: string = "white") { } private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") { + if (IS_REPORTER) { + console[level]("[Vencord]", this.name + ":", ...args); + return; + } + console[level]( `%c Vencord %c %c ${this.name} ${customFmt}`, `background: ${levelColor}; color: black; font-weight: bold; border-radius: 5px;`, diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts index f05900e1..d8c361e8 100644 --- a/src/utils/dependencies.ts +++ b/src/utils/dependencies.ts @@ -17,7 +17,6 @@ */ import { makeLazy } from "./lazy"; -import { EXTENSION_BASE_URL } from "./web-metadata"; /* Add dynamically loaded dependencies for plugins here. @@ -67,15 +66,6 @@ export interface ApngFrameData { playTime: number; } -// On web (extensions), use extension uri as basepath (load files from extension) -// On desktop (electron), load from cdn -export const rnnoiseDist = IS_EXTENSION - ? new URL("/third-party/rnnoise", EXTENSION_BASE_URL).toString() - : "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.3/dist"; -export const rnnoiseWasmSrc = (simd = false) => `${rnnoiseDist}/rnnoise${simd ? "_simd" : ""}.wasm`; -export const rnnoiseWorkletSrc = `${rnnoiseDist}/rnnoise/workletProcessor.js`; - - // The below code is only used on the Desktop (electron) build of Vencord. // Browser (extension) builds do not contain these remote imports. diff --git a/src/utils/types.ts b/src/utils/types.ts index 6e152419..fe19a109 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -94,6 +94,10 @@ export interface PluginDef { * @default StartAt.WebpackReady */ 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. * @deprecated Use `settings` instead @@ -144,6 +148,13 @@ export const enum StartAt { WebpackReady = "WebpackReady" } +export const enum ReporterTestable { + None = 1 << 1, + Start = 1 << 2, + Patches = 1 << 3, + FluxEvents = 1 << 4 +} + export const enum OptionType { STRING, NUMBER, diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx index 9a89af36..8957c254 100644 --- a/src/webpack/common/internal.tsx +++ b/src/webpack/common/internal.tsx @@ -22,7 +22,7 @@ import { LazyComponent } from "@utils/react"; import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack"; export function waitForComponent = React.ComponentType & Record>(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 () { throw new Error(`Vencord could not find the ${name} Component`); @@ -38,7 +38,7 @@ export function waitForComponent = React.Comp } 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 }); } diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts index 083ec269..f1fc68e8 100644 --- a/src/webpack/common/types/stores.d.ts +++ b/src/webpack/common/types/stores.d.ts @@ -41,8 +41,33 @@ export class FluxStore { __getLocalVars(): Record; } +export class FluxEmitter { + constructor(); + + changeSentinel: number; + changedStores: Set; + isBatchEmitting: boolean; + isDispatching: boolean; + isPaused: boolean; + pauseTimer: NodeJS.Timeout | null; + reactChangedStores: Set; + + 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 { Store: typeof FluxStore; + Emitter: FluxEmitter; } export class WindowStore extends FluxStore { diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 72a71f31..bb96861f 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -144,6 +144,7 @@ const persistFilter = filters.byCode("[zustand persist middleware]"); export const { persist: zustandPersist } = findLazy(m => m.persist && persistFilter(m.persist)); export const MessageActions = findByPropsLazy("editMessage", "sendMessage"); +export const MessageCache = findByPropsLazy("clearCache", "_channelMessages"); export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal"); export const InviteActions = findByPropsLazy("resolveInvite"); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 311e6f2b..da5ca8b9 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -209,7 +209,7 @@ function patchFactories(factories: Record findByProps("blah")); console.log(mod.blah); */ export function proxyLazyWebpack(factory: () => any, attempts?: number) { - if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); return proxyLazy(factory, attempts); } @@ -278,7 +278,7 @@ export function proxyLazyWebpack(factory: () => any, attempts?: number) * @returns Result of factory function */ export function LazyComponentWebpack(factory: () => any, attempts?: number) { - if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]); return LazyComponent(factory, attempts); } @@ -287,7 +287,7 @@ export function LazyComponentWebpack(factory: () => any, * Find the first module that matches the filter, lazily */ export function findLazy(filter: FilterFn) { - if (IS_DEV) lazyWebpackSearchHistory.push(["find", [filter]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["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 */ export function findByPropsLazy(...props: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["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 */ export function findByCodeLazy(...code: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByCode", code]); return proxyLazy(() => findByCode(...code)); } @@ -344,7 +344,7 @@ export function findStore(name: string) { * Find a store by its displayName, lazily */ export function findStoreLazy(name: string) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findStore", [name]]); return proxyLazy(() => findStore(name)); } @@ -363,7 +363,7 @@ export function findComponentByCode(...code: string[]) { * Finds the first component that matches the filter, lazily. */ export function findComponentLazy(filter: FilterFn) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponent", [filter]]); return LazyComponent(() => { @@ -378,7 +378,7 @@ export function findComponentLazy(filter: FilterFn) { * Finds the first component that includes all the given code, lazily */ export function findComponentByCodeLazy(...code: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponentByCode", code]); return LazyComponent(() => { const res = find(filters.componentByCode(...code), { isIndirect: true }); @@ -392,7 +392,7 @@ export function findComponentByCodeLazy(...code: string[ * Finds the first component that is exported by the first prop name, lazily */ export function findExportedComponentLazy(...props: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findExportedComponent", props]); return LazyComponent(() => { const res = find(filters.byProps(...props), { isIndirect: true }); @@ -402,14 +402,14 @@ export function findExportedComponentLazy(...props: stri }); } -export const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\))|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/; -export const ChunkIdsRegex = /\("(.+?)"\)/g; +export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/; +export const ChunkIdsRegex = /\("([^"]+?)"\)/g; /** * 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 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 - * @returns A promise that resolves when the chunks were loaded + * @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 with a boolean whether the chunks were loaded */ export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) { 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"); 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)); @@ -429,10 +433,10 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def if (IS_DEV && !devToolsOpen) throw err; - return; + return false; } - const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match; + const [, rawChunkIds, entryPointId] = match; 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"); logger.warn(err, "Code:", code, "Matcher:", matcher); @@ -441,16 +445,27 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def if (IS_DEV && !devToolsOpen) throw err; - return; + return false; } - const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle; if (rawChunkIds) { const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]); 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); + return true; } /** @@ -458,11 +473,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def * * 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 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 - * @returns A function that returns a promise that resolves when the chunks were loaded, on first call + * @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 with a boolean whether the chunks were loaded, on first call */ 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)); } @@ -472,7 +487,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac * then call the callback with the module as the first argument */ 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") filter = filters.byProps(filter);