Merge branch 'dev' into managed-styles-rewrite

This commit is contained in:
Sqaaakoi 2025-02-23 15:07:56 +13:00
commit 0d4e6795e0
No known key found for this signature in database
58 changed files with 1702 additions and 1959 deletions

View file

@ -2,30 +2,24 @@ name: Blank Issue
description: Create a blank issue. ALWAYS FIRST USE OUR SUPPORT CHANNEL! ONLY USE THIS FORM IF YOU ARE A CONTRIBUTOR OR WERE TOLD TO DO SO IN THE SUPPORT CHANNEL. description: Create a blank issue. ALWAYS FIRST USE OUR SUPPORT CHANNEL! ONLY USE THIS FORM IF YOU ARE A CONTRIBUTOR OR WERE TOLD TO DO SO IN THE SUPPORT CHANNEL.
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# READ THIS BEFORE OPENING AN ISSUE ![Are you a developer? No? This form is not for you!](https://github.com/Vendicated/Vencord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)
This form is ONLY FOR DEVELOPERS. YOUR ISSUE WILL BE CLOSED AND YOU WILL POSSIBLY BE BLOCKED FROM THE REPOSITORY IF YOU IGNORE THIS. GitHub Issues are for development, not support! Please use our [support server](https://vencord.dev/discord) unless you are a Vencord Developer.
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
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
- type: textarea - type: textarea
id: content id: content
attributes: attributes:
label: Content label: Content
validations: validations:
required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
options:
- label: I have read the requirements for opening an issue above
required: true required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
options:
- label: I have read the requirements for opening an issue above
required: true

View file

@ -4,78 +4,63 @@ labels: [bug]
title: "[Bug] <title>" title: "[Bug] <title>"
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# READ THIS BEFORE OPENING AN ISSUE ![Are you a developer? No? This form is not for you!](https://github.com/Vendicated/Vencord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)
This form is ONLY FOR DEVELOPERS. YOUR ISSUE WILL BE CLOSED AND YOU WILL POSSIBLY BE BLOCKED FROM THE REPOSITORY IF YOU IGNORE THIS. GitHub Issues are for development, not support! Please use our [support server](https://vencord.dev/discord) unless you are a Vencord Developer.
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
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
- type: input
id: discord
attributes:
label: Discord Account
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
placeholder: username#0000
validations:
required: false
- type: textarea - type: textarea
id: bug-description id: bug-description
attributes: attributes:
label: What happens when the bug or crash occurs? label: What happens when the bug or crash occurs?
description: Where does this bug or crash occur, when does it occur, etc. description: Where does this bug or crash occur, when does it occur, etc.
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ... placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
validations: validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: What is the expected behaviour?
description: Simply detail what the expected behaviour is.
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
validations:
required: true
- type: textarea
id: steps-to-take
attributes:
label: How do you recreate this bug or crash?
description: Give us a list of steps in order to recreate the bug or crash.
placeholder: |
1. Do ...
2. Then ...
3. Do this ..., ... and then ...
4. Observe "the bug" or "the crash"
validations:
required: true
- type: textarea
id: crash-log
attributes:
label: Errors
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
value: |
```
Replace this text with your crash-log.
```
validations:
required: false
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
options:
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
required: true required: true
- label: I have read the requirements for opening an issue above
- type: textarea
id: expected-behaviour
attributes:
label: What is the expected behaviour?
description: Simply detail what the expected behaviour is.
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
validations:
required: true required: true
- type: textarea
id: steps-to-take
attributes:
label: How do you recreate this bug or crash?
description: Give us a list of steps in order to recreate the bug or crash.
placeholder: |
1. Do ...
2. Then ...
3. Do this ..., ... and then ...
4. Observe "the bug" or "the crash"
validations:
required: true
- type: textarea
id: crash-log
attributes:
label: Errors
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
value: |
```
Replace this text with your crash-log.
```
validations:
required: false
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
options:
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
required: true
- label: I am a Vencord Developer
required: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -42,7 +42,7 @@ jobs:
- name: Clean up obsolete files - name: Clean up obsolete files
run: | run: |
rm -rf dist/*-unpacked dist/monaco Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map rm -rf dist/*-unpacked dist/vendor Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
- name: Get some values needed for the release - name: Get some values needed for the release
id: release_values id: release_values

View file

@ -36,7 +36,7 @@
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["dist/*", "third-party/*"], "resources": ["dist/*", "vendor/*"],
"matches": ["*://*.discord.com/*"] "matches": ["*://*.discord.com/*"]
} }
], ],

View file

@ -15,7 +15,7 @@ declare global {
const getTheme: () => string; const getTheme: () => string;
} }
const BASE = "/dist/monaco/vs"; const BASE = "/vendor/monaco/vs";
self.MonacoEnvironment = { self.MonacoEnvironment = {
getWorkerUrl(_moduleId: unknown, label: string) { getWorkerUrl(_moduleId: unknown, label: string) {

View file

@ -24,12 +24,12 @@
<script> <script>
const script = document.createElement("script"); const script = document.createElement("script");
script.src = new URL("/dist/monaco/index.js", baseUrl); script.src = new URL("/vendor/monaco/index.js", baseUrl);
const style = document.createElement("link"); const style = document.createElement("link");
style.type = "text/css"; style.type = "text/css";
style.rel = "stylesheet"; style.rel = "stylesheet";
style.href = new URL("/dist/monaco/index.css", baseUrl); style.href = new URL("/vendor/monaco/index.css", baseUrl);
document.body.append(style, script); document.body.append(style, script);
</script> </script>

View file

@ -134,7 +134,7 @@ export default tseslint.config(
"no-unsafe-optional-chaining": "error", "no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error", "no-useless-backreference": "error",
"use-isnan": "error", "use-isnan": "error",
"prefer-const": "error", "prefer-const": ["error", { destructuring: "all" }],
"prefer-spread": "error", "prefer-spread": "error",
// Plugin Rules // Plugin Rules

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.11.4", "version": "1.11.5",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -24,19 +24,18 @@
"dev": "pnpm watch", "dev": "pnpm watch",
"watchWeb": "pnpm buildWeb --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 --allowJs false",
"inject": "node scripts/runInstaller.mjs", "inject": "node scripts/runInstaller.mjs",
"uninject": "node scripts/runInstaller.mjs", "uninject": "node scripts/runInstaller.mjs",
"lint": "eslint", "lint": "eslint",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson", "test": "pnpm buildStandalone && pnpm testTsc && pnpm lint && pnpm lint-styles && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit" "testTsc": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@intrnl/xxhash64": "^0.1.2", "@intrnl/xxhash64": "^0.1.2",
"@sapphi-red/web-noise-suppressor": "0.3.5",
"@vap/core": "0.0.12", "@vap/core": "0.0.12",
"@vap/shiki": "0.10.5", "@vap/shiki": "0.10.5",
"fflate": "^0.8.2", "fflate": "^0.8.2",
@ -46,31 +45,31 @@
"virtual-merge": "^1.0.1" "virtual-merge": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^2.12.1", "@stylistic/eslint-plugin": "^4.0.0",
"@types/chrome": "^0.0.287", "@types/chrome": "^0.0.304",
"@types/diff": "^6.0.0", "@types/diff": "^7.0.1",
"@types/lodash": "^4.17.14", "@types/lodash": "^4.17.14",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/react": "^19.0.2", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.0.4",
"@types/yazl": "^2.4.5", "@types/yazl": "^2.4.5",
"diff": "^7.0.0", "diff": "^7.0.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"esbuild": "^0.15.18", "esbuild": "^0.25.0",
"eslint": "^9.17.0", "eslint": "^9.20.1",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "2.1.0", "eslint-plugin-path-alias": "2.1.0",
"eslint-plugin-react": "^7.37.3", "eslint-plugin-react": "^7.37.3",
"eslint-plugin-simple-header": "^1.2.1", "eslint-plugin-simple-header": "^1.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"highlight.js": "11.7.0", "highlight.js": "11.11.1",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"puppeteer-core": "^23.11.1", "puppeteer-core": "^24.2.1",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^34.2.0",
"stylelint": "^16.12.0", "stylelint": "^16.12.0",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^37.0.0",
"ts-patch": "^3.3.0", "ts-patch": "^3.3.0",
"ts-pattern": "^5.6.0", "ts-pattern": "^5.6.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
@ -80,10 +79,10 @@
"typescript-transform-paths": "^3.5.3", "typescript-transform-paths": "^3.5.3",
"zip-local": "^0.3.5" "zip-local": "^0.3.5"
}, },
"packageManager": "pnpm@9.1.0", "packageManager": "pnpm@10.4.1",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint@9.17.0": "patches/eslint@9.17.0.patch", "eslint@9.20.1": "patches/eslint@9.20.1.patch",
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch" "eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
}, },
"peerDependencyRules": { "peerDependencyRules": {
@ -96,18 +95,14 @@
"source-map-resolve": "*", "source-map-resolve": "*",
"resolve-url": "*", "resolve-url": "*",
"source-map-url": "*", "source-map-url": "*",
"urix": "*" "urix": "*",
} "q": "*"
},
"webExt": {
"artifactsDir": "./dist",
"build": {
"overwriteDest": true
}, },
"sourceDir": "./dist/firefox-unpacked" "onlyBuiltDependencies": [
"esbuild"
]
}, },
"engines": { "engines": {
"node": ">=18", "node": ">=18"
"pnpm": ">=9"
} }
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "@vencord/types", "name": "@vencord/types",
"private": false, "private": false,
"version": "0.1.3", "version": "1.11.5",
"description": "", "description": "",
"types": "index.d.ts", "types": "index.d.ts",
"scripts": { "scripts": {
@ -13,16 +13,16 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0", "fs-extra": "^11.3.0",
"tsx": "^3.12.6" "tsx": "^4.19.2"
}, },
"dependencies": { "dependencies": {
"@types/lodash": "^4.14.191", "@types/lodash": "4.17.15",
"@types/node": "^18.11.18", "@types/node": "^22.13.4",
"@types/react": "^18.2.0", "@types/react": "18.3.1",
"@types/react-dom": "^18.0.10", "@types/react-dom": "18.3.1",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^34.2.0",
"type-fest": "^3.5.3" "type-fest": "^4.35.0"
} }
} }

1922
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -17,38 +17,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import esbuild from "esbuild"; // @ts-check
import { readdir } from "fs/promises"; import { readdir } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from "./common.mjs";
const defines = { const defines = stringifyValues({
IS_STANDALONE, IS_STANDALONE,
IS_DEV, IS_DEV,
IS_REPORTER, IS_REPORTER,
IS_UPDATER_DISABLED, IS_UPDATER_DISABLED,
IS_WEB: false, IS_WEB: false,
IS_EXTENSION: false, IS_EXTENSION: false,
VERSION: JSON.stringify(VERSION), VERSION,
BUILD_TIMESTAMP BUILD_TIMESTAMP
}; });
if (defines.IS_STANDALONE === false) if (defines.IS_STANDALONE === "false") {
// If this is a local build (not standalone), optimize // 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);
}
/** /**
* @type {esbuild.BuildOptions} * @type {import("esbuild").BuildOptions}
*/ */
const nodeCommonOpts = { const nodeCommonOpts = {
...commonOpts, ...commonOpts,
define: defines,
format: "cjs", format: "cjs",
platform: "node", platform: "node",
target: ["esnext"], target: ["esnext"],
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external], // @ts-ignore this is never undefined
define: defines external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external]
}; };
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`; const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
@ -102,25 +105,27 @@ const globNativesPlugin = {
} }
}; };
await Promise.all([ /** @type {import("esbuild").BuildOptions[]} */
const buildConfigs = ([
// Discord Desktop main & renderer & preload // Discord Desktop main & renderer & preload
esbuild.build({ {
...nodeCommonOpts, ...nodeCommonOpts,
entryPoints: ["src/main/index.ts"], entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js", outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") }, footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap, sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
},
plugins: [ plugins: [
// @ts-ignore this is never undefined
...nodeCommonOpts.plugins, ...nodeCommonOpts.plugins,
globNativesPlugin globNativesPlugin
] ],
}), define: {
esbuild.build({ ...defines,
IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: "false"
}
},
{
...commonOpts, ...commonOpts,
entryPoints: ["src/Vencord.ts"], entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js", outfile: "dist/renderer.js",
@ -135,11 +140,11 @@ await Promise.all([
], ],
define: { define: {
...defines, ...defines,
IS_DISCORD_DESKTOP: true, IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: false IS_VESKTOP: "false"
} }
}), },
esbuild.build({ {
...nodeCommonOpts, ...nodeCommonOpts,
entryPoints: ["src/preload.ts"], entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js", outfile: "dist/preload.js",
@ -147,29 +152,29 @@ await Promise.all([
sourcemap, sourcemap,
define: { define: {
...defines, ...defines,
IS_DISCORD_DESKTOP: true, IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: false IS_VESKTOP: "false"
} }
}), },
// Vencord Desktop main & renderer & preload // Vencord Desktop main & renderer & preload
esbuild.build({ {
...nodeCommonOpts, ...nodeCommonOpts,
entryPoints: ["src/main/index.ts"], entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js", outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") }, footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap, sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
},
plugins: [ plugins: [
...nodeCommonOpts.plugins, ...nodeCommonOpts.plugins,
globNativesPlugin globNativesPlugin
] ],
}), define: {
esbuild.build({ ...defines,
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "true"
}
},
{
...commonOpts, ...commonOpts,
entryPoints: ["src/Vencord.ts"], entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js", outfile: "dist/vencordDesktopRenderer.js",
@ -184,11 +189,11 @@ await Promise.all([
], ],
define: { define: {
...defines, ...defines,
IS_DISCORD_DESKTOP: false, IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: true IS_VESKTOP: "true"
} }
}), },
esbuild.build({ {
...nodeCommonOpts, ...nodeCommonOpts,
entryPoints: ["src/preload.ts"], entryPoints: ["src/preload.ts"],
outfile: "dist/vencordDesktopPreload.js", outfile: "dist/vencordDesktopPreload.js",
@ -196,14 +201,10 @@ await Promise.all([
sourcemap, sourcemap,
define: { define: {
...defines, ...defines,
IS_DISCORD_DESKTOP: false, IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: true IS_VESKTOP: "true"
} }
}), }
]).catch(err => { ]);
console.error("Build failed");
console.error(err.message); await buildOrWatchAll(buildConfigs);
// make ci fail
if (!commonOpts.watch)
process.exitCode = 1;
});

View file

@ -17,29 +17,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import esbuild from "esbuild"; // @ts-check
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises"; 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, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs";
/** /**
* @type {esbuild.BuildOptions} * @type {import("esbuild").BuildOptions}
*/ */
const commonOptions = { const commonOptions = {
...commonOpts, ...commonOpts,
entryPoints: ["browser/Vencord.ts"], entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife", format: "iife",
globalName: "Vencord",
external: ["~plugins", "~git-hash", "/assets/*"], external: ["~plugins", "~git-hash", "/assets/*"],
target: ["esnext"],
plugins: [ plugins: [
globPlugins("web"), globPlugins("web"),
...commonRendererPlugins ...commonRendererPlugins
], ],
target: ["esnext"], define: stringifyValues({
define: {
IS_WEB: true, IS_WEB: true,
IS_EXTENSION: false, IS_EXTENSION: false,
IS_STANDALONE: true, IS_STANDALONE: true,
@ -48,9 +49,9 @@ const commonOptions = {
IS_DISCORD_DESKTOP: false, IS_DISCORD_DESKTOP: false,
IS_VESKTOP: false, IS_VESKTOP: false,
IS_UPDATER_DISABLED: true, IS_UPDATER_DISABLED: true,
VERSION: JSON.stringify(VERSION), VERSION,
BUILD_TIMESTAMP BUILD_TIMESTAMP
} })
}; };
const MonacoWorkerEntryPoints = [ const MonacoWorkerEntryPoints = [
@ -58,70 +59,59 @@ const MonacoWorkerEntryPoints = [
"vs/editor/editor.worker.js" "vs/editor/editor.worker.js"
]; ];
const RnNoiseFiles = [ /** @type {import("esbuild").BuildOptions[]} */
"dist/rnnoise.wasm", const buildConfigs = [
"dist/rnnoise_simd.wasm", {
"dist/rnnoise/workletProcessor.js", entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
"LICENSE" bundle: true,
minify: true,
format: "iife",
outbase: "node_modules/monaco-editor/esm/",
outdir: "dist/vendor/monaco"
},
{
entryPoints: ["browser/monaco.ts"],
bundle: true,
minify: true,
format: "iife",
outfile: "dist/vendor/monaco/index.js",
loader: {
".ttf": "file"
}
},
{
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }
},
{
...commonOptions,
outfile: "dist/extension.js",
define: {
...commonOptions.define,
IS_EXTENSION: "true"
},
footer: { js: "//# sourceURL=VencordWeb" }
},
{
...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
...commonOptions.define,
window: "unsafeWindow",
},
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${VERSION}.${new Date().getTime()}`)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
}
}
]; ];
await Promise.all( await buildOrWatchAll(buildConfigs);
[
esbuild.build({
entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
bundle: true,
minify: true,
format: "iife",
outbase: "node_modules/monaco-editor/esm/",
outdir: "dist/monaco"
}),
esbuild.build({
entryPoints: ["browser/monaco.ts"],
bundle: true,
minify: true,
format: "iife",
outfile: "dist/monaco/index.js",
loader: {
".ttf": "file"
}
}),
esbuild.build({
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
outfile: "dist/extension.js",
define: {
...commonOptions?.define,
IS_EXTENSION: true,
},
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
...(commonOptions?.define),
window: "unsafeWindow",
},
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${VERSION}.${new Date().getTime()}`)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
}
})
]
).catch(err => {
console.error("Build failed");
console.error(err.message);
if (!commonOpts.watch)
process.exit(1);
});;
/** /**
* @type {(dir: string) => Promise<string[]>} * @type {(dir: string) => Promise<string[]>}
@ -155,16 +145,13 @@ async function buildExtension(target, files) {
const entries = { const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"), "dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"), "dist/Vencord.css": await readFile("dist/extension.css"),
...await loadDir("dist/monaco"), ...await loadDir("dist/vendor/monaco", "dist/"),
...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file =>
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)]
))),
...Object.fromEntries(await Promise.all(files.map(async f => { ...Object.fromEntries(await Promise.all(files.map(async f => {
let content = await readFile(join("browser", f)); let content = await readFile(join("browser", f));
if (f.startsWith("manifest")) { if (f.startsWith("manifest")) {
const json = JSON.parse(content.toString("utf-8")); const json = JSON.parse(content.toString("utf-8"));
json.version = VERSION; json.version = VERSION;
content = new TextEncoder().encode(JSON.stringify(json)); content = Buffer.from(new TextEncoder().encode(JSON.stringify(json)));
} }
return [ return [
@ -208,7 +195,6 @@ if (!process.argv.includes("--skip-extension")) {
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 { } else {
await appendCssRuntime; await appendCssRuntime;
} }

View file

@ -16,11 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @ts-check
import "../suppressExperimentalWarnings.js"; import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js"; import "../checkNodeVersion.js";
import { exec, execSync } from "child_process"; import { exec, execSync } from "child_process";
import esbuild from "esbuild"; import esbuild, { build, context } from "esbuild";
import { constants as FsConstants, readFileSync } from "fs"; import { constants as FsConstants, readFileSync } from "fs";
import { access, readdir, readFile } from "fs/promises"; import { access, readdir, readFile } from "fs/promises";
import { minify as minifyHtml } from "html-minifier-terser"; import { minify as minifyHtml } from "html-minifier-terser";
@ -31,7 +33,7 @@ import { getPluginTarget } from "../utils.mjs";
import { builtinModules } from "module"; import { builtinModules } from "module";
/** @type {import("../../package.json")} */ /** @type {import("../../package.json")} */
const PackageJSON = JSON.parse(readFileSync("package.json")); const PackageJSON = JSON.parse(readFileSync("package.json", "utf-8"));
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/
@ -54,6 +56,34 @@ export const banner = {
`.trim() `.trim()
}; };
/**
* JSON.stringify all values in an object
* @type {(obj: Record<string, any>) => Record<string, string>}
*/
export function stringifyValues(obj) {
for (const key in obj) {
obj[key] = JSON.stringify(obj[key]);
}
return obj;
}
/**
* @param {import("esbuild").BuildOptions[]} buildConfigs
*/
export async function buildOrWatchAll(buildConfigs) {
if (watch) {
await Promise.all(buildConfigs.map(cfg =>
context(cfg).then(ctx => ctx.watch())
));
} else {
await Promise.all(buildConfigs.map(cfg => build(cfg)))
.catch(error => {
console.error(error.message);
process.exit(1); // exit immediately to skip the rest of the builds
});
}
}
const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/; const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/;
/** /**
* @param {string} base * @param {string} base
@ -311,18 +341,16 @@ export const banImportPlugin = (filter, message) => ({
export const commonOpts = { export const commonOpts = {
logLevel: "info", logLevel: "info",
bundle: true, bundle: true,
watch,
minify: !watch && !IS_REPORTER, minify: !watch && !IS_REPORTER,
sourcemap: watch ? "inline" : "", sourcemap: watch ? "inline" : "external",
legalComments: "linked", legalComments: "linked",
banner, banner,
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin], plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"], external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"], inject: ["./scripts/build/inject/react.mjs"],
jsx: "transform",
jsxFactory: "VencordCreateElement", jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment", jsxFragment: "VencordFragment"
// Work around https://github.com/evanw/esbuild/issues/2460
tsconfig: "./scripts/build/tsconfig.esbuild.json"
}; };
const escapedBuiltinModules = builtinModules const escapedBuiltinModules = builtinModules
@ -335,5 +363,6 @@ export const commonRendererPlugins = [
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"), banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"), banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"), banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
// @ts-ignore this is never undefined
...commonOpts.plugins ...commonOpts.plugins
]; ];

View file

@ -1,7 +0,0 @@
// Work around https://github.com/evanw/esbuild/issues/2460
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}

View file

@ -16,11 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* eslint-disable no-fallthrough */
// eslint-disable-next-line spaced-comment
/// <reference types="../src/globals" /> /// <reference types="../src/globals" />
// eslint-disable-next-line spaced-comment
/// <reference types="../src/modules" /> /// <reference types="../src/modules" />
import { createHmac } from "crypto"; import { createHmac } from "crypto";
@ -58,14 +54,17 @@ async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
.catch(() => undefined); .catch(() => undefined);
} }
interface PatchInfo {
plugin: string;
type: string;
id: string;
match: string;
error?: string;
};
const report = { const report = {
badPatches: [] as { badPatches: [] as PatchInfo[],
plugin: string; slowPatches: [] as PatchInfo[],
type: string;
id: string;
match: string;
error?: string;
}[],
badStarts: [] as { badStarts: [] as {
plugin: string; plugin: string;
error: string; error: string;
@ -136,53 +135,67 @@ async function printReport() {
console.log(); console.log();
if (process.env.WEBHOOK_URL) { if (process.env.WEBHOOK_URL) {
const patchesToEmbed = (title: string, patches: PatchInfo[], color: number) => ({
title,
color,
description: patches.map(p => {
const lines = [
`**__${p.plugin} (${p.type}):__**`,
`ID: \`${p.id}\``,
`Match: ${toCodeBlock(p.match, "Match: ".length, true)}`
];
if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`);
return lines.join("\n");
}).join("\n\n"),
});
const embeds = [
{
author: {
name: `Discord ${CANARY ? "Canary" : "Stable"} (${metaData.buildNumber})`,
url: `https://nelly.tools/builds/app/${metaData.buildHash}`,
icon_url: CANARY ? "https://cdn.discordapp.com/emojis/1252721945699549327.png?size=128" : "https://cdn.discordapp.com/emojis/1252721943463985272.png?size=128"
},
color: CANARY ? 0xfbb642 : 0x5865f2
},
report.badPatches.length > 0 && patchesToEmbed("Bad Patches", report.badPatches, 0xff0000),
report.slowPatches.length > 0 && patchesToEmbed("Slow Patches", report.slowPatches, 0xf0b232),
report.badWebpackFinds.length > 0 && {
title: "Bad Webpack Finds",
description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None",
color: 0xff0000
},
report.badStarts.length > 0 && {
title: "Bad Starts",
description: report.badStarts.map(p => {
const lines = [
`**__${p.plugin}:__**`,
toCodeBlock(p.error, 0, true)
];
return lines.join("\n");
}
).join("\n\n") || "None",
color: 0xff0000
},
report.otherErrors.length > 0 && {
title: "Discord Errors",
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None",
color: 0xff0000
}
].filter(Boolean);
if (embeds.length === 1) {
embeds.push({
title: "No issues found",
description: "Seems like everything is working fine (for now) <:shipit:1330992641466433556>",
color: 0x00ff00
});
}
const body = JSON.stringify({ const body = JSON.stringify({
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""), username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
embeds: [ embeds
{
author: {
name: `Discord ${CANARY ? "Canary" : "Stable"} (${metaData.buildNumber})`,
url: `https://nelly.tools/builds/app/${metaData.buildHash}`,
icon_url: CANARY ? "https://cdn.discordapp.com/emojis/1252721945699549327.png?size=128" : "https://cdn.discordapp.com/emojis/1252721943463985272.png?size=128"
},
color: CANARY ? 0xfbb642 : 0x5865f2
},
{
title: "Bad Patches",
description: report.badPatches.map(p => {
const lines = [
`**__${p.plugin} (${p.type}):__**`,
`ID: \`${p.id}\``,
`Match: ${toCodeBlock(p.match, "Match: ".length, true)}`
];
if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`);
return lines.join("\n");
}).join("\n\n") || "None",
color: report.badPatches.length ? 0xff0000 : 0x00ff00
},
{
title: "Bad Webpack Finds",
description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None",
color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00
},
{
title: "Bad Starts",
description: report.badStarts.map(p => {
const lines = [
`**__${p.plugin}:__**`,
toCodeBlock(p.error, 0, true)
];
return lines.join("\n");
}
).join("\n\n") || "None",
color: report.badStarts.length ? 0xff0000 : 0x00ff00
},
{
title: "Discord Errors",
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None",
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
}
]
}); });
const headers = { const headers = {
@ -249,14 +262,17 @@ page.on("console", async e => {
switch (tag) { switch (tag) {
case "WebpackInterceptor:": case "WebpackInterceptor:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module|took [\d.]+?ms) \(Module id is (.+?)\): (.+)/)!; const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/);
if (!patchFailMatch) break; const patchSlowMatch = message.match(/Patch by (.+?) (took [\d.]+?ms) \(Module id is (.+?)\): (.+)/);
const match = patchFailMatch ?? patchSlowMatch;
if (!match) break;
logStderr(await getText()); logStderr(await getText());
process.exitCode = 1; process.exitCode = 1;
const [, plugin, type, id, regex] = patchFailMatch; const [, plugin, type, id, regex] = match;
report.badPatches.push({ const list = patchFailMatch ? report.badPatches : report.slowPatches;
list.push({
plugin, plugin,
type, type,
id, id,

View file

@ -4,11 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import type { Settings } from "@api/Settings";
import { PluginIpcMappings } from "@main/ipcPlugins"; import { PluginIpcMappings } from "@main/ipcPlugins";
import type { UserThemeHeader } from "@main/themes"; import type { UserThemeHeader } from "@main/themes";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { IpcRes } from "@utils/types"; import { IpcRes } from "@utils/types";
import type { Settings } from "api/Settings";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
function invoke<T = any>(event: IpcEvents, ...args: any[]) { function invoke<T = any>(event: IpcEvents, ...args: any[]) {

View file

@ -65,7 +65,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
} }
const canonicalMatch = canonicalizeMatch(new RegExp(match)); const canonicalMatch = canonicalizeMatch(new RegExp(match));
try { try {
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin"); const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins["YourPlugin"]');
var patched = src.replace(canonicalMatch, canonicalReplace as string); var patched = src.replace(canonicalMatch, canonicalReplace as string);
setReplacementError(void 0); setReplacementError(void 0);
} catch (e) { } catch (e) {

View file

@ -8,7 +8,7 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { wreq } from "@webpack"; import { wreq } from "@webpack";
import { AnyModuleFactory, ModuleFactory } from "webpack"; import { AnyModuleFactory, ModuleFactory } from "@webpack/wreq.d";
export async function loadLazyChunks() { export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
@ -20,8 +20,7 @@ export async function loadLazyChunks() {
const invalidChunks = new Set<PropertyKey>(); const invalidChunks = new Set<PropertyKey>();
const deferredRequires = new Set<PropertyKey>(); const deferredRequires = new Set<PropertyKey>();
let chunksSearchingResolve: (value: void) => void; const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
// True if resolved, false otherwise // True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>; const chunksSearchPromises = [] as Array<() => boolean>;
@ -69,7 +68,7 @@ export async function loadLazyChunks() {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id)) const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text()) .then(r => r.text())
.then(t => t.includes("importScripts(")); .then(t => /importScripts\(|self\.postMessage/.test(t));
if (isWorkerAsset) { if (isWorkerAsset) {
invalidChunks.add(id); invalidChunks.add(id);
@ -140,8 +139,8 @@ export async function loadLazyChunks() {
} }
Webpack.factoryListeners.add(factoryListener); Webpack.factoryListeners.add(factoryListener);
for (const factoryId in wreq.m) { for (const moduleId in wreq.m) {
factoryListener(wreq.m[factoryId]); factoryListener(wreq.m[moduleId]);
} }
await chunksSearchingDone; await chunksSearchingDone;
@ -175,7 +174,7 @@ export async function loadLazyChunks() {
await Promise.all(chunksLeft.map(async id => { await Promise.all(chunksLeft.map(async id => {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id)) const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text()) .then(r => r.text())
.then(t => t.includes("importScripts(")); .then(t => /importScripts\(|self\.postMessage/.test(t));
// Loads the chunk. Currently this only happens with the language packs which are loaded differently // Loads the chunk. Currently this only happens with the language packs which are loaded differently
if (!isWorkerAsset) { if (!isWorkerAsset) {

View file

@ -6,9 +6,9 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { addPatch, patches } from "plugins"; import { getBuildNumber, patchTimings } from "@webpack/patcher";
import { getBuildNumber } from "webpack/patchWebpack";
import { addPatch, patches } from "../plugins";
import { loadLazyChunks } from "./loadLazyChunks"; import { loadLazyChunks } from "./loadLazyChunks";
async function runReporter() { async function runReporter() {
@ -17,8 +17,7 @@ async function runReporter() {
try { try {
ReporterLogger.log("Starting test..."); ReporterLogger.log("Starting test...");
let loadLazyChunksResolve: (value: void) => void; const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers<void>();
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
// The main patch for starting the reporter chunk loading // The main patch for starting the reporter chunk loading
addPatch({ addPatch({
@ -51,7 +50,7 @@ async function runReporter() {
} }
} }
for (const [plugin, moduleId, match, totalTime] of Vencord.WebpackPatcher.patchTimings) { for (const [plugin, moduleId, match, totalTime] of patchTimings) {
if (totalTime > 5) { if (totalTime > 5) {
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`); new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
} }
@ -79,9 +78,9 @@ async function runReporter() {
result = await Webpack.extractAndLoadChunks(code, matcher); result = await Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null; if (result === false) result = null;
} else if (method === "mapMangledModule") { } else if (method === "mapMangledModule") {
const [code, mapper] = args; const [code, mapper, includeBlacklistedExports] = args;
result = Webpack.mapMangledModule(code, mapper); result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports);
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail"); if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
} else { } else {
// @ts-ignore // @ts-ignore

View file

@ -16,8 +16,8 @@ export default definePlugin({
{ {
find: "SCALE_DOWN:", find: "SCALE_DOWN:",
replacement: { replacement: {
match: /!\(null==(\i)\|\|0===\i\|\|null==(\i)\|\|0===\i\)/, match: /(?<="IMAGE"===\i\?)\i(?=\?)/,
replace: (_, width, height) => `!((null == ${width} || 0 === ${width}) && (null == ${height} || 0 === ${height}))` replace: "true"
} }
} }
] ]

View file

@ -20,7 +20,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 definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { WebpackRequire } from "webpack"; import { WebpackRequire } from "@webpack/wreq.d";
const settings = definePluginSettings({ const settings = definePluginSettings({
disableAnalytics: { disableAnalytics: {

View file

@ -158,6 +158,9 @@ export default definePlugin({
aboveActivity: getIntlMessage("ACTIVITY_SETTINGS") aboveActivity: getIntlMessage("ACTIVITY_SETTINGS")
}; };
if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS"))
return firstChild === "PREMIUM";
return header === names[settingsLocation]; return header === names[settingsLocation];
} catch { } catch {
return firstChild === "PREMIUM"; return firstChild === "PREMIUM";

View file

@ -85,7 +85,7 @@ export default definePlugin({
replace: "$&onRequestClose:$self.onPopoutClose," replace: "$&onRequestClose:$self.onPopoutClose,"
}, },
{ {
match: /(?<=\.avatarWrapper,)/, match: /(?<=#{intl::SET_STATUS}\),)/,
replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu," replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
} }
] ]

View file

@ -185,8 +185,8 @@ export default definePlugin({
{ {
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar // Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always, predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.isExpanded\),children:\[)/, match: /\.isExpanded\),.{0,30}children:\[/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&" replace: "$&$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
}, },
{ {
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar // Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar

View file

@ -26,10 +26,18 @@ export default definePlugin({
patches: [ patches: [
{ {
find: '"ChannelAttachButton"', find: '"ChannelAttachButton"',
replacement: { replacement: [
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/, {
replace: "$&onClick:$1,onContextMenu:$2.onClick,", // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
}, match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
noWarn: true
},
{
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,100}\},(\i)\).{0,100}children:\i/,
replace: "$&,onClick:$1,onContextMenu:$2.onClick,",
},
]
}, },
], ],
}); });

View file

@ -50,12 +50,24 @@ export default definePlugin({
find: ".decorationGridItem,", find: ".decorationGridItem,",
replacement: [ replacement: [
{ {
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/, // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
replace: "$self.DecorationGridItem=$&" match: /(?<==)\i=>{let{children.{20,200}decorationGridItem/,
replace: "$self.DecorationGridItem=$&",
noWarn: true
}, },
{ {
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /(?<==)\i=>{let{user:\i,avatarDecoration/, match: /(?<==)\i=>{let{user:\i,avatarDecoration/,
replace: "$self.DecorationGridDecoration=$&" replace: "$self.DecorationGridDecoration=$&",
noWarn: true
},
{
match: /(?<==)\i=>{var{children.{20,200}decorationGridItem/,
replace: "$self.DecorationGridItem=$&",
},
{
match: /(?<==)\i=>{var{user:\i,avatarDecoration/,
replace: "$self.DecorationGridDecoration=$&",
}, },
// Remove NEW label from decor avatar decorations // Remove NEW label from decor avatar decorations
{ {

View file

@ -5,7 +5,7 @@
*/ */
import { CopyIcon, DeleteIcon } from "@components/Icons"; import { CopyIcon, DeleteIcon } from "@components/Icons";
import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "webpack/common"; import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "@webpack/common";
import { Decoration } from "../../lib/api"; import { Decoration } from "../../lib/api";
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";

View file

@ -173,7 +173,7 @@ function initWs(isManual = false) {
try { try {
const matcher = canonicalizeMatch(parseNode(match)); const matcher = canonicalizeMatch(parseNode(match));
const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName"); const replacement = canonicalizeReplace(parseNode(replace), 'Vencord.Plugins.plugins["PlaceHolderPluginName"]');
const newSource = src.replace(matcher, replacement as string); const newSource = src.replace(matcher, replacement as string);

View file

@ -9,7 +9,7 @@ import { app } from "electron";
app.on("browser-window-created", (_, win) => { app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => { win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => { frame?.once("dom-ready", () => {
if (frame.url.startsWith("https://open.spotify.com/embed/")) { if (frame.url.startsWith("https://open.spotify.com/embed/")) {
const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds; const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds;
if (!settings?.enabled) return; if (!settings?.enabled) return;

View file

@ -9,7 +9,7 @@ import { app } from "electron";
app.on("browser-window-created", (_, win) => { app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => { win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => { frame?.once("dom-ready", () => {
if (frame.url.startsWith("https://www.youtube.com/")) { if (frame.url.startsWith("https://www.youtube.com/")) {
const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds; const settings = RendererSettings.store.plugins?.FixYoutubeEmbeds;
if (!settings?.enabled) return; if (!settings?.enabled) return;

View file

@ -31,7 +31,7 @@ export default definePlugin({
{ {
name: "create friend invite", name: "create friend invite",
description: "Generates a friend invite link.", description: "Generates a friend invite link.",
inputType: ApplicationCommandInputType.BOT, inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (args, ctx) => { execute: async (args, ctx) => {
const invite = await FriendInvites.createFriendInvite(); const invite = await FriendInvites.createFriendInvite();
@ -48,7 +48,7 @@ export default definePlugin({
{ {
name: "view friend invites", name: "view friend invites",
description: "View a list of all generated friend invites.", description: "View a list of all generated friend invites.",
inputType: ApplicationCommandInputType.BOT, inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (_, ctx) => { execute: async (_, ctx) => {
const invites = await FriendInvites.getAllFriendInvites(); const invites = await FriendInvites.getAllFriendInvites();
const friendInviteList = invites.map(i => const friendInviteList = invites.map(i =>
@ -67,7 +67,7 @@ export default definePlugin({
{ {
name: "revoke friend invites", name: "revoke friend invites",
description: "Revokes all generated friend invites.", description: "Revokes all generated friend invites.",
inputType: ApplicationCommandInputType.BOT, inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (_, ctx) => { execute: async (_, ctx) => {
await FriendInvites.revokeFriendInvites(); await FriendInvites.revokeFriendInvites();

View file

@ -12,7 +12,7 @@ import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack"; import { findStoreLazy } from "@webpack";
import { Button, Forms, showToast, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common"; import { Button, Forms, showToast, TextInput, Toasts, Tooltip, useEffect, useState } from "@webpack/common";
const enum ActivitiesTypes { const enum ActivitiesTypes {
Game, Game,

View file

@ -34,7 +34,7 @@ export default definePlugin({
{ {
find: "#{intl::FRIENDS_ALL_HEADER}", find: "#{intl::FRIENDS_ALL_HEADER}",
replacement: { replacement: {
match: /toString\(\)\}\);case (\i\.\i)\.BLOCKED/, match: /toString\(\)\}\);case (\i\.\i)\.PENDING/,
replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED' replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED'
}, },
}, },
@ -50,7 +50,7 @@ export default definePlugin({
{ {
find: "#{intl::FRIENDS_SECTION_ONLINE}", find: "#{intl::FRIENDS_SECTION_ONLINE}",
replacement: { replacement: {
match: /,{id:(\i\.\i)\.BLOCKED,show:.+?className:(\i\.item)/, match: /,{id:(\i\.\i)\.PENDING,show:.+?className:(\i\.item)/,
replace: (rest, relationShipTypes, className) => `,{id:${relationShipTypes}.IMPLICIT,show:true,className:${className},content:"Implicit"}${rest}` replace: (rest, relationShipTypes, className) => `,{id:${relationShipTypes}.IMPLICIT,show:true,className:${className},content:"Implicit"}${rest}`
} }
}, },
@ -58,7 +58,7 @@ export default definePlugin({
{ {
find: '"FriendsStore"', find: '"FriendsStore"',
replacement: { replacement: {
match: /(?<=case (\i\.\i)\.BLOCKED:return (\i)\.type===\i\.\i\.BLOCKED)/, match: /(?<=case (\i\.\i)\.SUGGESTIONS:return \d+===(\i)\.type)/,
replace: ";case $1.IMPLICIT:return $2.type===5" replace: ";case $1.IMPLICIT:return $2.type===5"
}, },
}, },

View file

@ -31,6 +31,7 @@ import { Logger } from "@utils/Logger";
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches"; import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types"; import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
import { patches } from "@webpack/patcher";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
import Plugins from "~plugins"; import Plugins from "~plugins";
@ -41,7 +42,7 @@ const logger = new Logger("PluginManager", "#a6d189");
export const PMLogger = logger; export const PMLogger = logger;
export const plugins = Plugins; export const plugins = Plugins;
export const patches = [] as Patch[]; export { patches };
/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */ /** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */
let enabledPluginsSubscribedFlux = false; let enabledPluginsSubscribedFlux = false;

View file

@ -65,10 +65,18 @@ export default definePlugin({
patches: [ patches: [
{ {
find: "{isSidebarVisible:", find: "{isSidebarVisible:",
replacement: { replacement: [
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, {
replace: ":[$1?.startsWith('members')?$self.render():null,$2" // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
}, match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2",
noWarn: true
},
{
match: /(?<=var\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2",
},
],
predicate: () => settings.store.memberList predicate: () => settings.store.memberList
}, },
{ {

View file

@ -84,8 +84,14 @@ export default definePlugin({
find: ".USER_MENTION)", find: ".USER_MENTION)",
replacement: [ replacement: [
{ {
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/, match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/,
replace: "$&,color:$self.getColorInt($1?.id,$2?.id)" replace: "$&,color:$self.getColorInt($1?.id,$2?.id)",
noWarn: true
},
{
match: /(?<=onContextMenu:\i,color:)\i(?=\},\i\),\{children)(?<=user:(\i),channel:(\i).{0,500}?)/,
replace: "$self.getColorInt($1?.id,$2?.id)",
} }
], ],
predicate: () => settings.store.chatMentions predicate: () => settings.store.chatMentions

View file

@ -96,6 +96,6 @@
.vc-shiki-root .vc-shiki-table-cell:last-child { .vc-shiki-root .vc-shiki-table-cell:last-child {
padding-left: 8px; padding-left: 8px;
word-break: break-word; overflow-wrap: break-word;
width: 100%; width: 100%;
} }

View file

@ -173,7 +173,7 @@ export default definePlugin({
replacement: [ replacement: [
// Make the channel appear as muted if it's hidden // Make the channel appear as muted if it's hidden
{ {
match: /{channel:(\i),name:\i,muted:(\i).+?;/, match: /\.subtitle,.+?;(?=return\(0,\i\.jsxs?\))(?<={channel:(\i),name:\i,muted:(\i).+?;)/,
replace: (m, channel, muted) => `${m}${muted}=$self.isHiddenChannel(${channel})?true:${muted};` replace: (m, channel, muted) => `${m}${muted}=$self.isHiddenChannel(${channel})?true:${muted};`
}, },
// Add the hidden eye icon if the channel is hidden // Add the hidden eye icon if the channel is hidden
@ -204,7 +204,7 @@ export default definePlugin({
{ {
// Hide unreads // Hide unreads
predicate: () => settings.store.hideUnreads === true, predicate: () => settings.store.hideUnreads === true,
match: /{channel:(\i),name:\i,.+?unread:(\i).+?;/, match: /\.subtitle,.+?;(?=return\(0,\i\.jsxs?\))(?<={channel:(\i),name:\i,.+?unread:(\i).+?)/,
replace: (m, channel, unread) => `${m}${unread}=$self.isHiddenChannel(${channel})?false:${unread};` replace: (m, channel, unread) => `${m}${unread}=$self.isHiddenChannel(${channel})?false:${unread};`
} }
] ]
@ -485,7 +485,7 @@ export default definePlugin({
} }
}, },
{ {
find: '="NowPlayingViewStore",', find: '"NowPlayingViewStore"',
replacement: { replacement: {
// Make active now voice states on hidden channels // Make active now voice states on hidden channels
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/, match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,

View file

@ -77,7 +77,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
class SpotifyStore extends Store { class SpotifyStore extends Store {
public mPosition = 0; public mPosition = 0;
private start = 0; public _start = 0;
public track: Track | null = null; public track: Track | null = null;
public device: Device | null = null; public device: Device | null = null;
@ -100,26 +100,26 @@ export const SpotifyStore = proxyLazyWebpack(() => {
public get position(): number { public get position(): number {
let pos = this.mPosition; let pos = this.mPosition;
if (this.isPlaying) { if (this.isPlaying) {
pos += Date.now() - this.start; pos += Date.now() - this._start;
} }
return pos; return pos;
} }
public set position(p: number) { public set position(p: number) {
this.mPosition = p; this.mPosition = p;
this.start = Date.now(); this._start = Date.now();
} }
prev() { prev() {
this.req("post", "/previous"); this._req("post", "/previous");
} }
next() { next() {
this.req("post", "/next"); this._req("post", "/next");
} }
setVolume(percent: number) { setVolume(percent: number) {
this.req("put", "/volume", { this._req("put", "/volume", {
query: { query: {
volume_percent: Math.round(percent) volume_percent: Math.round(percent)
} }
@ -131,17 +131,17 @@ export const SpotifyStore = proxyLazyWebpack(() => {
} }
setPlaying(playing: boolean) { setPlaying(playing: boolean) {
this.req("put", playing ? "/play" : "/pause"); this._req("put", playing ? "/play" : "/pause");
} }
setRepeat(state: Repeat) { setRepeat(state: Repeat) {
this.req("put", "/repeat", { this._req("put", "/repeat", {
query: { state } query: { state }
}); });
} }
setShuffle(state: boolean) { setShuffle(state: boolean) {
this.req("put", "/shuffle", { this._req("put", "/shuffle", {
query: { state } query: { state }
}).then(() => { }).then(() => {
this.shuffle = state; this.shuffle = state;
@ -154,7 +154,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
this.isSettingPosition = true; this.isSettingPosition = true;
return this.req("put", "/seek", { return this._req("put", "/seek", {
query: { query: {
position_ms: Math.round(ms) position_ms: Math.round(ms)
} }
@ -164,7 +164,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
}); });
} }
private req(method: "post" | "get" | "put", route: string, data: any = {}) { _req(method: "post" | "get" | "put", route: string, data: any = {}) {
if (this.device?.is_active) if (this.device?.is_active)
(data.query ??= {}).device_id = this.device.id; (data.query ??= {}).device_id = this.device.id;

View file

@ -27,12 +27,22 @@ export default definePlugin({
authors: [Devs.Megu], authors: [Devs.Megu],
patches: [{ patches: [{
find: "#{intl::ACTIVITY_SETTINGS}", find: "#{intl::ACTIVITY_SETTINGS}",
replacement: { replacement: [
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/, {
replace: (_, commaOrSemi, settings, elements) => "" + // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
`${commaOrSemi}${settings}?.[0]==="CHANGELOG"` + match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/,
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})` replace: (_, commaOrSemi, settings, elements) => "" +
} `${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`,
noWarn: true
},
{
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+\)\)\}\))(?=\)\})/,
replace: (_, commaOrSemi, settings, elements) => "" +
`${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`,
},
]
}], }],
StartupTimingPage StartupTimingPage
}); });

View file

@ -41,11 +41,20 @@ export default definePlugin({
}, },
{ {
find: '="SYSTEM_TAG"', find: '="SYSTEM_TAG"',
replacement: { replacement: [
// Add next to username (compact mode) {
match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\),(?=\i)/g, // Add next to username (compact mode)
replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0])," // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
} match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\),(?=\i)/g,
replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0]),",
noWarn: true
},
{
// Add next to username (compact mode)
match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\)\),(?=\i)/g,
replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0]),",
},
]
} }
], ],

View file

@ -128,7 +128,7 @@ function VoiceChannelTooltip({ channel, isLocked }: VoiceChannelTooltipProps) {
); );
} }
interface VoiceChannelIndicatorProps { export interface VoiceChannelIndicatorProps {
userId: string; userId: string;
isActionButton?: boolean; isActionButton?: boolean;
shouldHighlight?: boolean; shouldHighlight?: boolean;

View file

@ -193,10 +193,18 @@ export default definePlugin({
// Avatar component used in User DMs "User Profile" popup in the right and Profiles Modal pfp // Avatar component used in User DMs "User Profile" popup in the right and Profiles Modal pfp
{ {
find: ".overlay:void 0,status:", find: ".overlay:void 0,status:",
replacement: { replacement: [
match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/, {
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)}," // FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
}, match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/,
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},",
noWarn: true
},
{
match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",.{0,100}className:\i,/,
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},",
}
],
all: true all: true
}, },
// Banners // Banners

View file

@ -10,7 +10,7 @@ import adguard from "file://adguard.js?minify";
app.on("browser-window-created", (_, win) => { app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => { win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => { frame?.once("dom-ready", () => {
if (!RendererSettings.store.plugins?.YoutubeAdblock?.enabled) return; if (!RendererSettings.store.plugins?.YoutubeAdblock?.enabled) return;
if (frame.url.includes("youtube.com/embed/") || (frame.url.includes("discordsays") && frame.url.includes("youtube.com"))) { if (frame.url.includes("youtube.com/embed/") || (frame.url.includes("discordsays") && frame.url.includes("youtube.com"))) {

View file

@ -17,7 +17,6 @@
@media(width <= 485px) { @media(width <= 485px) {
.vc-image-modal { .vc-image-modal {
display: relative;
overflow: visible; overflow: visible;
overflow: initial; overflow: initial;
} }

View file

@ -20,9 +20,9 @@ export function makeLazy<T>(factory: () => T, attempts = 5): () => T {
let tries = 0; let tries = 0;
let cache: T; let cache: T;
return () => { return () => {
if (!cache && attempts > tries++) { if (cache === undefined && attempts > tries++) {
cache = factory(); cache = factory();
if (!cache && attempts === tries) if (cache === undefined && attempts === tries)
console.error("Lazy factory failed:", factory); console.error("Lazy factory failed:", factory);
} }
return cache; return cache;

View file

@ -42,7 +42,12 @@ export interface PatchReplacement {
match: string | RegExp; match: string | RegExp;
/** The replacement string or function which returns the string for the patch replacement */ /** The replacement string or function which returns the string for the patch replacement */
replace: string | ReplaceFn; replace: string | ReplaceFn;
/** A function which returns whether this patch replacement should be applied */ /** Do not warn if this replacement did no changes */
noWarn?: boolean;
/**
* A function which returns whether this patch replacement should be applied.
* This is ran before patches are registered, so if this returns false, the patch will never be registered.
*/
predicate?(): boolean; predicate?(): boolean;
/** The minimum build number for this patch to be applied */ /** The minimum build number for this patch to be applied */
fromBuild?: number; fromBuild?: number;
@ -62,7 +67,10 @@ export interface Patch {
noWarn?: boolean; noWarn?: boolean;
/** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */ /** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */
group?: boolean; group?: boolean;
/** A function which returns whether this patch should be applied */ /**
* A function which returns whether this patch replacement should be applied.
* This is ran before patches are registered, so if this returns false, the patch will never be registered.
*/
predicate?(): boolean; predicate?(): boolean;
/** The minimum build number for this patch to be applied */ /** The minimum build number for this patch to be applied */
fromBuild?: number; fromBuild?: number;

View file

@ -41,7 +41,7 @@ export const Switch = waitForComponent<t.Switch>("Switch", filters.componentByCo
const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", { const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", {
Tooltip: filters.componentByCode("this.renderTooltip()]"), Tooltip: filters.componentByCode("this.renderTooltip()]"),
TooltipContainer: filters.componentByCode('="div",') TooltipContainer: filters.componentByCode('="div"')
}) as { }) as {
Tooltip: t.Tooltip, Tooltip: t.Tooltip,
TooltipContainer: t.TooltipContainer; TooltipContainer: t.TooltipContainer;

View file

@ -29,7 +29,7 @@ export type GenericStore = t.FluxStore & Record<string, any>;
export const DraftType = findByPropsLazy("ChannelMessage", "SlashCommand"); export const DraftType = findByPropsLazy("ChannelMessage", "SlashCommand");
export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & { export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & GenericStore & {
getMessages(chanId: string): any; getMessages(chanId: string): any;
}; };

View file

@ -496,7 +496,7 @@ export type Avatar = ComponentType<PropsWithChildren<{
}>>; }>>;
type FocusLock = ComponentType<PropsWithChildren<{ type FocusLock = ComponentType<PropsWithChildren<{
containerRef: RefObject<HTMLElement>; containerRef: Ref<HTMLElement>;
}>>; }>>;
export type Icon = ComponentType<JSX.IntrinsicElements["svg"] & { export type Icon = ComponentType<JSX.IntrinsicElements["svg"] & {

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { DraftType } from "@webpack/common";
import { Channel, Guild, Role } from "discord-types/general"; import { Channel, Guild, Role } from "discord-types/general";
import { FluxDispatcher, FluxEvents } from "./utils"; import { FluxDispatcher, FluxEvents } from "./utils";
@ -234,7 +233,7 @@ export class PopoutWindowStore extends FluxStore {
} }
export type useStateFromStores = <T>( export type useStateFromStores = <T>(
stores: t.FluxStore[], stores: any[],
mapper: () => T, mapper: () => T,
dependencies?: any, dependencies?: any,
isEqual?: (old: T, newer: T) => boolean isEqual?: (old: T, newer: T) => boolean

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { runtimeHashMessageKey } from "@utils/intlHash";
import type { Channel } from "discord-types/general"; import type { Channel } from "discord-types/general";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
@ -58,9 +57,9 @@ export const { match, P }: Pick<typeof import("ts-pattern"), "match" | "P"> = ma
export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep"); export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep");
export const i18n = mapMangledModuleLazy('defaultLocale:"en-US"', { export const i18n = mapMangledModuleLazy('defaultLocale:"en-US"', {
intl: filters.byProps("string", "format"), t: m => m?.[Symbol.toStringTag] === "IntlMessagesProxy",
t: filters.byProps(runtimeHashMessageKey("DISCORD")) intl: m => m != null && Object.getPrototypeOf(m)?.withFormatters != null
}); }, true);
export let SnowflakeUtils: t.SnowflakeUtils; export let SnowflakeUtils: t.SnowflakeUtils;
waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m);

View file

@ -9,63 +9,63 @@ import { makeLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { interpolateIfDefined } from "@utils/misc"; import { interpolateIfDefined } from "@utils/misc";
import { canonicalizeReplacement } from "@utils/patches"; import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types"; import { Patch, PatchReplacement } from "@utils/types";
import { traceFunctionWithResults } from "../debug/Tracer"; import { traceFunctionWithResults } from "../debug/Tracer";
import { patches } from "../plugins"; import { _blacklistBadModules, _initWebpack, factoryListeners, findModuleFactory, moduleListeners, waitForSubscriptions, wreq } from "./webpack";
import { _initWebpack, _shouldIgnoreModule, AnyModuleFactory, AnyWebpackRequire, factoryListeners, findModuleId, MaybeWrappedModuleFactory, ModuleExports, moduleListeners, waitForSubscriptions, WebpackRequire, WrappedModuleFactory, wreq } from "."; import { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, PatchedModuleFactory, WebpackRequire } from "./wreq.d";
export const patches = [] as Patch[];
export const SYM_IS_PROXIED_FACTORY = Symbol("WebpackPatcher.isProxiedFactory");
export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory"); export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory");
export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource"); export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource");
export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy"); export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy");
/** A set with all the Webpack instances */
export const allWebpackInstances = new Set<AnyWebpackRequire>(); export const allWebpackInstances = new Set<AnyWebpackRequire>();
export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: string | RegExp, totalTime: number]>;
const logger = new Logger("WebpackInterceptor", "#8caaee"); export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: PatchReplacement["match"], totalTime: number]>;
/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */
let wreqFallbackApplied = false;
/** Whether we should be patching factories.
*
* This should be disabled if we start searching for the module to get the build number, and then resumed once it's done.
* */
let shouldPatchFactories = true;
export const getBuildNumber = makeLazy(() => { export const getBuildNumber = makeLazy(() => {
try { try {
shouldPatchFactories = false; function matchBuildNumber(factoryStr: string) {
const buildNumberMatch = factoryStr.match(/.concat\("(\d+?)"\)/);
try { if (buildNumberMatch == null) {
if (wreq.m[128014]?.toString().includes("Trying to open a changelog for an invalid build number")) { return -1;
const hardcodedGetBuildNumber = wreq(128014).b as () => number;
if (typeof hardcodedGetBuildNumber === "function" && typeof hardcodedGetBuildNumber() === "number") {
return hardcodedGetBuildNumber();
}
} }
} catch { }
const moduleId = findModuleId("Trying to open a changelog for an invalid build number"); return Number(buildNumberMatch[1]);
if (moduleId == null) {
return -1;
} }
const exports = Object.values<ModuleExports>(wreq(moduleId)); const hardcodedFactoryStr = String(wreq.m[128014]);
if (exports.length !== 1 || typeof exports[0] !== "function") { if (hardcodedFactoryStr.includes("Trying to open a changelog for an invalid build number")) {
return -1; const hardcodedBuildNumber = matchBuildNumber(hardcodedFactoryStr);
if (hardcodedBuildNumber !== -1) {
return hardcodedBuildNumber;
}
} }
const buildNumber = exports[0](); const moduleFactory = findModuleFactory("Trying to open a changelog for an invalid build number");
return typeof buildNumber === "number" ? buildNumber : -1; return matchBuildNumber(String(moduleFactory));
} catch { } catch {
return -1; return -1;
} finally {
shouldPatchFactories = true;
} }
}); });
type Define = typeof Reflect.defineProperty; export function getFactoryPatchedSource(moduleId: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
const define: Define = (target, p, attributes) => { return webpackRequire.m[moduleId]?.[SYM_PATCHED_SOURCE];
}
export function getFactoryPatchedBy(moduleId: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
return webpackRequire.m[moduleId]?.[SYM_PATCHED_BY];
}
const logger = new Logger("WebpackInterceptor", "#8caaee");
/** Whether we tried to fallback to the WebpackRequire of the factory, or disabled patches */
let wreqFallbackApplied = false;
const define: typeof Reflect.defineProperty = (target, p, attributes) => {
if (Object.hasOwn(attributes, "value")) { if (Object.hasOwn(attributes, "value")) {
attributes.writable = true; attributes.writable = true;
} }
@ -77,22 +77,18 @@ const define: Define = (target, p, attributes) => {
}); });
}; };
export function getOriginalFactory(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { // wreq.m is the Webpack object containing module factories. It is pre-populated with factories, and is also populated via webpackGlobal.push
const moduleFactory = webpackRequire.m[id]; // We use this setter to intercept when wreq.m is defined and setup our setters which decide whether we should patch these module factories
return (moduleFactory?.[SYM_ORIGINAL_FACTORY] ?? moduleFactory) as AnyModuleFactory | undefined; // and the Webpack instance where they are being defined.
}
export function getFactoryPatchedSource(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { // Factories can be patched in two ways. Eagerly or lazily.
return webpackRequire.m[id]?.[SYM_PATCHED_SOURCE]; // If we are patching eagerly, pre-populated factories are patched immediately and new factories are patched when set.
} // Else, we only patch them when called.
export function getFactoryPatchedBy(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { // Factories are always wrapped in a proxy, which allows us to intercept the call to them, patch if they werent eagerly patched,
return webpackRequire.m[id]?.[SYM_PATCHED_BY]; // and call them with our wrapper which notifies our listeners.
}
// wreq.m is the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push // wreq.m is also wrapped in a proxy to intercept when new factories are set, patch them eargely, if enabled, and wrap them in the factory proxy.
// We use this setter to intercept when wreq.m is defined and apply the patching in its module factories.
// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or defining getters for the patched versions.
// If this is the main Webpack, we also set up the internal references to WebpackRequire. // If this is the main Webpack, we also set up the internal references to WebpackRequire.
define(Function.prototype, "m", { define(Function.prototype, "m", {
@ -101,7 +97,7 @@ define(Function.prototype, "m", {
set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) { set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) {
define(this, "m", { value: originalModules }); define(this, "m", { value: originalModules });
// Ensure this is one of Discord main Webpack instances. // Ensure this is likely one of Discord main Webpack instances.
// We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here. // We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here.
const { stack } = new Error(); const { stack } = new Error();
if (!stack?.includes("http") || stack.match(/at \d+? \(/) || !String(this).includes("exports:{}")) { if (!stack?.includes("http") || stack.match(/at \d+? \(/) || !String(this).includes("exports:{}")) {
@ -109,53 +105,108 @@ define(Function.prototype, "m", {
} }
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1]; const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
allWebpackInstances.add(this); // Define a setter for the bundlePath property of WebpackRequire. Only Webpack instances which include chunk loading functionality,
// like the main Discord Webpack, have this property.
// Define a setter for the ensureChunk property of WebpackRequire. Only the main Webpack (which is the only that includes chunk loading) has this property. // So if the setter is called with the Discord bundlePath, this means we should patch this instance and initialize the internal references to WebpackRequire.
// So if the setter is called, this means we can initialize the internal references to WebpackRequire. define(this, "p", {
define(this, "e", {
enumerable: false, enumerable: false,
set(this: WebpackRequire, ensureChunk: WebpackRequire["e"]) { set(this: AnyWebpackRequire, bundlePath: NonNullable<AnyWebpackRequire["p"]>) {
define(this, "e", { value: ensureChunk }); define(this, "p", { value: bundlePath });
clearTimeout(setterTimeout); clearTimeout(bundlePathTimeout);
logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); if (bundlePath !== "/assets/") {
_initWebpack(this); return;
}
if (wreq == null && this.c != null) {
logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire");
_initWebpack(this as WebpackRequire);
}
patchThisInstance();
} }
}); });
// setImmediate to clear this property setter if this is not the main Webpack.
// If this is the main Webpack, wreq.e will always be set before the timeout runs.
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "e"), 0);
// Patch the pre-populated factories // In the past, the sentry Webpack instance which we also wanted to patch used to rely on chunks being loaded before initting sentry.
for (const id in originalModules) { // This Webpack instance did not include actual chunk loading, and only awaited for them to be loaded, which means it did not include the bundlePath property.
if (updateExistingFactory(originalModules, id, originalModules[id], true)) { // To keep backwards compability, in case this is ever the case again, and keep patching this type of instance, we explicity patch instances which include wreq.O and not wreq.p.
continue; // Since we cannot check what is the bundlePath of the instance to filter for the Discord bundlePath, we only patch it if wreq.p is not included,
// which means the instance relies on another instance which does chunk loading, and that makes it very likely to only target Discord Webpack instances like the old sentry.
// Instead of patching when wreq.O is defined, wait for when wreq.O.j is defined, since that will be one of the last things to happen,
// which can assure wreq.p could have already been defined before.
define(this, "O", {
enumerable: false,
set(this: AnyWebpackRequire, onChunksLoaded: NonNullable<AnyWebpackRequire["O"]>) {
define(this, "O", { value: onChunksLoaded });
clearTimeout(onChunksLoadedTimeout);
const wreq = this;
define(onChunksLoaded, "j", {
enumerable: false,
set(this: NonNullable<AnyWebpackRequire["O"]>, j: NonNullable<AnyWebpackRequire["O"]>["j"]) {
define(this, "j", { value: j });
if (wreq.p == null) {
patchThisInstance();
}
}
});
} }
notifyFactoryListeners(originalModules[id]);
defineModulesFactoryGetter(id, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]);
}
define(originalModules, Symbol.toStringTag, {
value: "ModuleFactories",
enumerable: false
}); });
// The proxy responsible for patching the module factories when they are set, or defining getters for the patched versions // If neither of these properties setters were triggered, delete them as they are not needed anymore.
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler); const bundlePathTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
/* const onChunksLoadedTimeout = setTimeout(() => Reflect.deleteProperty(this, "O"), 0);
If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));
*/
define(this, "m", { value: proxiedModuleFactories }); /**
* Patch the current Webpack instance assigned to `this` context.
* This should only be called if this instance was later found to be one we need to patch.
*/
const patchThisInstance = () => {
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
allWebpackInstances.add(this);
// Proxy (and maybe patch) pre-populated factories
for (const moduleId in originalModules) {
updateExistingOrProxyFactory(originalModules, moduleId, originalModules[moduleId], originalModules, true);
}
define(originalModules, Symbol.toStringTag, {
value: "ModuleFactories",
enumerable: false
});
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);
/*
If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));
*/
define(this, "m", { value: proxiedModuleFactories });
// Overwrite Webpack's defineExports function to define the export descriptors configurable.
// This is needed so we can later blacklist specific exports from Webpack search by making them non-enumerable
this.d = function (exports, definition) {
for (const key in definition) {
if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) {
Object.defineProperty(exports, key, {
enumerable: true,
configurable: true,
get: definition[key],
});
}
}
};
};
} }
}); });
// The proxy for patching eagerly and/or wrapping factories in their proxy.
const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = { const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
/* /*
If Webpack ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype If Webpack ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype
@ -172,71 +223,127 @@ const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
}, },
*/ */
// The set trap for patching or defining getters for the module factories when new module factories are loaded set: updateExistingOrProxyFactory
set(target, p, newValue, receiver) { };
if (updateExistingFactory(target, p, newValue)) {
// The proxy for patching lazily and/or running factories with our wrapper.
const moduleFactoryHandler: ProxyHandler<MaybePatchedModuleFactory> = {
apply(target, thisArg: unknown, argArray: Parameters<AnyModuleFactory>) {
// SYM_ORIGINAL_FACTORY means the factory has already been patched
if (target[SYM_ORIGINAL_FACTORY] != null) {
return runFactoryWithWrap(target as PatchedModuleFactory, thisArg, argArray);
}
// SAFETY: Factories have `name` as their key in the module factories object, and that is always their module id
const moduleId: string = target.name;
const patchedFactory = patchFactory(moduleId, target);
return runFactoryWithWrap(patchedFactory, thisArg, argArray);
},
get(target, p, receiver) {
if (p === SYM_IS_PROXIED_FACTORY) {
return true; return true;
} }
notifyFactoryListeners(newValue); const originalFactory: AnyModuleFactory = target[SYM_ORIGINAL_FACTORY] ?? target;
defineModulesFactoryGetter(p, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(p, newValue) : newValue);
return true; // Redirect these properties to the original factory, including making `toString` return the original factory `toString`
if (p === "toString" || p === SYM_PATCHED_SOURCE || p === SYM_PATCHED_BY) {
const v = Reflect.get(originalFactory, p, originalFactory);
return p === "toString" ? v.bind(originalFactory) : v;
}
return Reflect.get(target, p, receiver);
} }
}; };
function updateExistingOrProxyFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget = false) {
if (updateExistingFactory(moduleFactories, moduleId, newFactory, receiver, ignoreExistingInTarget)) {
return true;
}
notifyFactoryListeners(moduleId, newFactory);
const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, newFactory) : newFactory, moduleFactoryHandler);
return Reflect.set(moduleFactories, moduleId, proxiedFactory, receiver);
}
/** /**
* Update a factory that exists in any Webpack instance with a new original factory. * Update a duplicated factory that exists in any of the Webpack instances we track with a new original factory.
* *
* @target The module factories where this new original factory is being set * @param moduleFactories The module factories where this new original factory is being set
* @param id The id of the module * @param moduleId The id of the module
* @param newFactory The new original factory * @param newFactory The new original factory
* @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget * @param receiver The receiver of the factory
* @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance * @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactories where it is being set
* @returns Whether the original factory was updated, or false if it doesn't exist in any of the tracked Webpack instances
*/ */
function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) { function updateExistingFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget) {
let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined; let existingFactory: AnyModuleFactory | undefined;
let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined; let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined;
for (const wreq of allWebpackInstances) { for (const wreq of allWebpackInstances) {
if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue; if (ignoreExistingInTarget && wreq.m === moduleFactories) {
continue;
}
if (Object.hasOwn(wreq.m, id)) { if (Object.hasOwn(wreq.m, moduleId)) {
existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id); existingFactory = wreq.m[moduleId];
moduleFactoriesWithFactory = wreq.m; moduleFactoriesWithFactory = wreq.m;
break; break;
} }
} }
if (existingFactory != null) { if (existingFactory != null) {
// If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required. // If existingFactory exists in any of the Webpack instances we track, it's either wrapped in our proxy, or it has already been required.
// So define the descriptor of it on this current Webpack instance (if it doesn't exist already), call Reflect.set with the new original, // In the case it is wrapped in our proxy, and the instance we are setting does not already have it, we need to make sure the instance contains our proxy too.
// and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) if (moduleFactoriesWithFactory !== moduleFactories && existingFactory[SYM_IS_PROXIED_FACTORY]) {
Reflect.set(moduleFactories, moduleId, existingFactory, receiver);
if (moduleFactoriesWithFactory !== moduleFactoriesTarget) { }
Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory); // Else, if it is not wrapped in our proxy, set this new original factory in all the instances
else {
defineInWebpackInstances(moduleId, newFactory);
} }
// Persist patched source and patched by in the new original factory, if the patched one has already been required // Update existingFactory with the new original, if it does have a current original factory
if (IS_DEV && existingFactory.value != null) { if (existingFactory[SYM_ORIGINAL_FACTORY] != null) {
newFactory[SYM_PATCHED_SOURCE] = existingFactory.value[SYM_PATCHED_SOURCE]; existingFactory[SYM_ORIGINAL_FACTORY] = newFactory;
newFactory[SYM_PATCHED_BY] = existingFactory.value[SYM_PATCHED_BY];
} }
return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget); // Persist patched source and patched by in the new original factory
if (IS_DEV) {
newFactory[SYM_PATCHED_SOURCE] = existingFactory[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = existingFactory[SYM_PATCHED_BY];
}
return true;
} }
return false; return false;
} }
/**
* Define a module factory in all the Webpack instances we track.
*
* @param moduleId The id of the module
* @param factory The factory
*/
function defineInWebpackInstances(moduleId: PropertyKey, factory: AnyModuleFactory) {
for (const wreq of allWebpackInstances) {
define(wreq.m, moduleId, { value: factory });
}
}
/** /**
* Notify all factory listeners. * Notify all factory listeners.
* *
* @param moduleId The id of the module
* @param factory The original factory to notify for * @param factory The original factory to notify for
*/ */
function notifyFactoryListeners(factory: AnyModuleFactory) { function notifyFactoryListeners(moduleId: PropertyKey, factory: AnyModuleFactory) {
for (const factoryListener of factoryListeners) { for (const factoryListener of factoryListeners) {
try { try {
factoryListener(factory); factoryListener(factory, moduleId);
} catch (err) { } catch (err) {
logger.error("Error in Webpack factory listener:\n", err, factoryListener); logger.error("Error in Webpack factory listener:\n", err, factoryListener);
} }
@ -244,213 +351,148 @@ function notifyFactoryListeners(factory: AnyModuleFactory) {
} }
/** /**
* Define the getter for returning the patched version of the module factory. * Run a (possibly) patched module factory with a wrapper which notifies our listeners.
* *
* If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original * @param patchedFactory The (possibly) patched module factory
* and only be patched when accessed for the first time. * @param thisArg The `value` of the call to the factory
* * @param argArray The arguments of the call to the factory
* @param id The id of the module
* @param factory The original or patched module factory
*/ */
function defineModulesFactoryGetter(id: PropertyKey, factory: MaybeWrappedModuleFactory) { function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unknown, argArray: Parameters<MaybePatchedModuleFactory>) {
const descriptor: PropertyDescriptor = { const originalFactory = patchedFactory[SYM_ORIGINAL_FACTORY];
get() {
// SYM_ORIGINAL_FACTORY means the factory is already patched
if (!shouldPatchFactories || factory[SYM_ORIGINAL_FACTORY] != null) {
return factory;
}
return (factory = wrapAndPatchFactory(id, factory)); if (patchedFactory === originalFactory) {
}, // @ts-expect-error Clear up ORIGINAL_FACTORY if the factory did not have any patch applied
set(newFactory: MaybeWrappedModuleFactory) { delete patchedFactory[SYM_ORIGINAL_FACTORY];
if (IS_DEV) {
newFactory[SYM_PATCHED_SOURCE] = factory[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = factory[SYM_PATCHED_BY];
}
if (factory[SYM_ORIGINAL_FACTORY] != null) {
factory.toString = newFactory.toString.bind(newFactory);
factory[SYM_ORIGINAL_FACTORY] = newFactory;
} else {
factory = newFactory;
}
}
};
// Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object
// have the patched version
for (const wreq of allWebpackInstances) {
define(wreq.m, id, descriptor);
} }
}
/** let [module, exports, require] = argArray;
* Wraps and patches a module factory.
*
* @param id The id of the module
* @param factory The original or patched module factory
* @returns The wrapper for the patched module factory
*/
function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) {
const [patchedFactory, patchedSource, patchedBy] = patchFactory(id, originalFactory);
const wrappedFactory: WrappedModuleFactory = function (...args) { // Restore the original factory in all the module factories objects, discarding our proxy and allowing it to be garbage collected
// Restore the original factory in all the module factories objects. We want to make sure the original factory is restored properly, no matter what is the Webpack instance defineInWebpackInstances(module.id, originalFactory);
for (const wreq of allWebpackInstances) {
define(wreq.m, id, { value: wrappedFactory[SYM_ORIGINAL_FACTORY] });
}
// eslint-disable-next-line prefer-const if (wreq == null) {
let [module, exports, require] = args; if (!wreqFallbackApplied) {
wreqFallbackApplied = true;
if (wreq == null) { // Make sure the require argument is actually the WebpackRequire function
if (!wreqFallbackApplied) { if (typeof require === "function" && require.m != null && require.c != null) {
wreqFallbackApplied = true; const { stack } = new Error();
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
// Make sure the require argument is actually the WebpackRequire function logger.warn(
if (typeof require === "function" && require.m != null) { "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called wrapped module factory (" +
const { stack } = new Error(); `id: ${String(module.id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; ")"
);
logger.warn( // Could technically be wrong, but it's better than nothing
"WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + _initWebpack(require as WebpackRequire);
`id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
")"
);
_initWebpack(require as WebpackRequire);
} else if (IS_DEV) {
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
}
} else if (IS_DEV) { } else if (IS_DEV) {
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args); logger.error("WebpackRequire was not initialized, running modules without patches instead.");
return originalFactory.apply(thisArg, argArray);
} }
} else if (IS_DEV) {
return originalFactory.apply(thisArg, argArray);
}
}
let factoryReturn: unknown;
try {
factoryReturn = patchedFactory.apply(thisArg, argArray);
} catch (err) {
// Just re-throw Discord errors
if (patchedFactory === originalFactory) {
throw err;
} }
let factoryReturn: unknown; logger.error("Error in patched module factory:\n", err);
try { return originalFactory.apply(thisArg, argArray);
// Call the patched factory }
factoryReturn = patchedFactory.apply(this, args);
} catch (err) {
// Just re-throw Discord errors
if (patchedFactory === wrappedFactory[SYM_ORIGINAL_FACTORY]) {
throw err;
}
logger.error("Error in patched module factory:\n", err); exports = module.exports;
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
}
exports = module.exports; if (typeof require === "function" && require.c) {
if (exports == null) { if (_blacklistBadModules(require.c, exports, module.id)) {
return factoryReturn; return factoryReturn;
} }
if (typeof require === "function") {
const shouldIgnoreModule = _shouldIgnoreModule(exports);
if (shouldIgnoreModule) {
if (require.c != null) {
Object.defineProperty(require.c, id, {
value: require.c[id],
enumerable: false,
configurable: true,
writable: true
});
}
return factoryReturn;
}
}
for (const callback of moduleListeners) {
try {
callback(exports, id);
} catch (err) {
logger.error("Error in Webpack module listener:\n", err, callback);
}
}
for (const [filter, callback] of waitForSubscriptions) {
try {
if (filter(exports)) {
waitForSubscriptions.delete(filter);
callback(exports, id);
continue;
}
if (typeof exports !== "object") {
continue;
}
for (const exportKey in exports) {
const exportValue = exports[exportKey];
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, id);
break;
}
}
} catch (err) {
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
}
}
return factoryReturn;
};
wrappedFactory.toString = originalFactory.toString.bind(originalFactory);
wrappedFactory[SYM_ORIGINAL_FACTORY] = originalFactory;
if (IS_DEV && patchedFactory !== originalFactory) {
wrappedFactory[SYM_PATCHED_SOURCE] = patchedSource;
wrappedFactory[SYM_PATCHED_BY] = patchedBy;
originalFactory[SYM_PATCHED_SOURCE] = patchedSource;
originalFactory[SYM_PATCHED_BY] = patchedBy;
} }
// @ts-expect-error Allow GC to get into action, if possible if (exports == null) {
originalFactory = undefined; return factoryReturn;
return wrappedFactory; }
for (const callback of moduleListeners) {
try {
callback(exports, module.id);
} catch (err) {
logger.error("Error in Webpack module listener:\n", err, callback);
}
}
for (const [filter, callback] of waitForSubscriptions) {
try {
if (filter(exports)) {
waitForSubscriptions.delete(filter);
callback(exports, module.id);
continue;
}
if (typeof exports !== "object") {
continue;
}
for (const exportKey in exports) {
const exportValue = exports[exportKey];
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, module.id);
break;
}
}
} catch (err) {
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
}
}
return factoryReturn;
} }
/** /**
* Patches a module factory. * Patches a module factory.
* *
* @param id The id of the module * @param moduleId The id of the module
* @param factory The original module factory * @param originalFactory The original module factory
* @returns The patched module factory, the patched source of it, and the plugins that patched it * @returns The patched module factory
*/ */
function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFactory: AnyModuleFactory, patchedSource: string, patchedBy: Set<string>] { function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory): PatchedModuleFactory {
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0, // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + String(factory); let code: string = "0," + String(originalFactory);
let patchedSource = code; let patchedSource = code;
let patchedFactory = factory; let patchedFactory = originalFactory;
const patchedBy = new Set<string>(); const patchedBy = new Set<string>();
for (let i = 0; i < patches.length; i++) { for (let i = 0; i < patches.length; i++) {
const patch = patches[i]; const patch = patches[i];
const moduleMatches = typeof patch.find === "string" const buildNumber = getBuildNumber();
? code.includes(patch.find) const shouldCheckBuildNumber = buildNumber !== -1;
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
if (!moduleMatches) {
continue;
}
// Reporter eagerly patches and cannot retrieve the build number because this code runs before the module for it is loaded
const buildNumber = IS_REPORTER ? -1 : getBuildNumber();
const shouldCheckBuildNumber = !Settings.eagerPatches && buildNumber !== -1;
if ( if (
shouldCheckBuildNumber && shouldCheckBuildNumber &&
(patch.fromBuild != null && buildNumber < patch.fromBuild) || (patch.fromBuild != null && buildNumber < patch.fromBuild) ||
(patch.toBuild != null && buildNumber > patch.toBuild) (patch.toBuild != null && buildNumber > patch.toBuild)
) { ) {
patches.splice(i--, 1);
continue;
}
const moduleMatches = typeof patch.find === "string"
? code.includes(patch.find)
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
if (!moduleMatches) {
continue; continue;
} }
@ -463,7 +505,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
}); });
const previousCode = code; const previousCode = code;
const previousFactory = factory; const previousFactory = originalFactory;
let markedAsPatched = false; let markedAsPatched = false;
// We change all patch.replacement to array in plugins/index // We change all patch.replacement to array in plugins/index
@ -482,18 +524,18 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
} }
const lastCode = code; const lastCode = code;
const lastFactory = factory; const lastFactory = originalFactory;
try { try {
const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string); const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
if (IS_REPORTER) { if (IS_REPORTER) {
patchTimings.push([patch.plugin, id, replacement.match, totalTime]); patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);
} }
if (newCode === code) { if (newCode === code) {
if (!patch.noWarn) { if (!(patch.noWarn || replacement.noWarn)) {
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(id)}): ${replacement.match}`); logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);
if (IS_DEV) { if (IS_DEV) {
logger.debug("Function Source:\n", code); logger.debug("Function Source:\n", code);
} }
@ -514,8 +556,13 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
continue; continue;
} }
const pluginsList = [...patchedBy];
if (!patchedBy.has(patch.plugin)) {
pluginsList.push(patch.plugin);
}
code = newCode; code = newCode;
patchedSource = `// Webpack Module ${String(id)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`; patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(moduleId)}`;
patchedFactory = (0, eval)(patchedSource); patchedFactory = (0, eval)(patchedSource);
if (!patchedBy.has(patch.plugin)) { if (!patchedBy.has(patch.plugin)) {
@ -523,7 +570,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
markedAsPatched = true; markedAsPatched = true;
} }
} catch (err) { } catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err); logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\n`, err);
if (IS_DEV) { if (IS_DEV) {
diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!); diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);
@ -550,7 +597,14 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
} }
} }
return [patchedFactory, patchedSource, patchedBy]; patchedFactory[SYM_ORIGINAL_FACTORY] = originalFactory;
if (IS_DEV && patchedFactory !== originalFactory) {
originalFactory[SYM_PATCHED_SOURCE] = patchedSource;
originalFactory[SYM_PATCHED_BY] = patchedBy;
}
return patchedFactory as PatchedModuleFactory;
} }
function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) { function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {

View file

@ -22,7 +22,8 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import { traceFunction } from "../debug/Tracer"; import { traceFunction } from "../debug/Tracer";
import { AnyModuleFactory, ModuleExports, WebpackRequire } from "./wreq"; import { Flux } from "./common";
import { AnyModuleFactory, AnyWebpackRequire, ModuleExports, WebpackRequire } from "./wreq";
const logger = new Logger("Webpack"); const logger = new Logger("Webpack");
@ -90,17 +91,13 @@ export const filters = {
}; };
export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void; export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void;
export type FactoryListernFn = (factory: AnyModuleFactory) => void; export type FactoryListernFn = (factory: AnyModuleFactory, moduleId: PropertyKey) => void;
export const waitForSubscriptions = new Map<FilterFn, CallbackFn>(); export const waitForSubscriptions = new Map<FilterFn, CallbackFn>();
export const moduleListeners = new Set<CallbackFn>(); export const moduleListeners = new Set<CallbackFn>();
export const factoryListeners = new Set<FactoryListernFn>(); export const factoryListeners = new Set<FactoryListernFn>();
export function _initWebpack(webpackRequire: WebpackRequire) { export function _initWebpack(webpackRequire: WebpackRequire) {
if (webpackRequire.c == null) {
return;
}
wreq = webpackRequire; wreq = webpackRequire;
cache = webpackRequire.c; cache = webpackRequire.c;
@ -115,18 +112,38 @@ export function _initWebpack(webpackRequire: WebpackRequire) {
// Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too // Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too
const TypedArray = Object.getPrototypeOf(Int8Array); const TypedArray = Object.getPrototypeOf(Int8Array);
function _shouldIgnoreValue(value: any) { const PROXY_CHECK = "is this a proxy that returns values for any key?";
function shouldIgnoreValue(value: any) {
if (value == null) return true; if (value == null) return true;
if (value === window) return true; if (value === window) return true;
if (value === document || value === document.documentElement) return true; if (value === document || value === document.documentElement) return true;
if (value[Symbol.toStringTag] === "DOMTokenList") return true; if (value[Symbol.toStringTag] === "DOMTokenList" || value[Symbol.toStringTag] === "IntlMessagesProxy") return true;
// Discord might export a Proxy that returns non-null values for any property key which would pass all findByProps filters.
// One example of this is their i18n Proxy. However, that is already covered by the IntlMessagesProxy check above.
// As a fallback if they ever change the name or add a new Proxy, use a unique string to detect such proxies and ignore them
if (value[PROXY_CHECK] !== void 0) {
// their i18n Proxy "caches" by setting each accessed property to the return, so try to delete
Reflect.deleteProperty(value, PROXY_CHECK);
return true;
}
if (value instanceof TypedArray) return true; if (value instanceof TypedArray) return true;
return false; return false;
} }
export function _shouldIgnoreModule(exports: any) { function makePropertyNonEnumerable(target: Record<PropertyKey, any>, key: PropertyKey) {
if (_shouldIgnoreValue(exports)) { const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor == null) return;
Reflect.defineProperty(target, key, {
...descriptor,
enumerable: false
});
}
export function _blacklistBadModules(requireCache: NonNullable<AnyWebpackRequire["c"]>, exports: ModuleExports, moduleId: PropertyKey) {
if (shouldIgnoreValue(exports)) {
makePropertyNonEnumerable(requireCache, moduleId);
return true; return true;
} }
@ -134,14 +151,16 @@ export function _shouldIgnoreModule(exports: any) {
return false; return false;
} }
let allNonEnumerable = true; let hasOnlyBadProperties = true;
for (const exportKey in exports) { for (const exportKey in exports) {
if (!_shouldIgnoreValue(exports[exportKey])) { if (shouldIgnoreValue(exports[exportKey])) {
allNonEnumerable = false; makePropertyNonEnumerable(exports, exportKey);
} else {
hasOnlyBadProperties = false;
} }
} }
return allNonEnumerable; return hasOnlyBadProperties;
} }
let devToolsOpen = false; let devToolsOpen = false;
@ -409,7 +428,10 @@ export function findByCodeLazy(...code: CodeFilter) {
* Find a store by its displayName * Find a store by its displayName
*/ */
export function findStore(name: StoreNameFilter) { export function findStore(name: StoreNameFilter) {
const res = find(filters.byStoreName(name), { isIndirect: true }); const res = Flux.Store.getAll
? Flux.Store.getAll().find(filters.byStoreName(name))
: find(filters.byStoreName(name), { isIndirect: true });
if (!res) if (!res)
handleModuleNotFound("findStore", name); handleModuleNotFound("findStore", name);
return res; return res;
@ -477,12 +499,27 @@ export function findExportedComponentLazy<T extends object = any>(...props: Prop
}); });
} }
function getAllPropertyNames(object: Record<PropertyKey, any>, includeNonEnumerable: boolean) {
const names = new Set<PropertyKey>();
const getKeys = includeNonEnumerable ? Object.getOwnPropertyNames : Object.keys;
do {
getKeys(object).forEach(name => name !== "__esModule" && names.add(name));
object = Object.getPrototypeOf(object);
} while (object != null);
return names;
}
/** /**
* Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module) * Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module)
* then maps it into an easily usable module via the specified mappers. * then maps it into an easily usable module via the specified mappers.
* *
* @param code The code to look for * @param code The code to look for
* @param mappers Mappers to create the non mangled exports * @param mappers Mappers to create the non mangled exports
* @param includeBlacklistedExports Whether to include blacklisted exports in the search.
* These exports are dangerous. Accessing properties on them may throw errors
* or always return values (so a byProps filter will always return true)
* @returns Unmangled exports as specified in mappers * @returns Unmangled exports as specified in mappers
* *
* @example mapMangledModule("headerIdIsManaged:", { * @example mapMangledModule("headerIdIsManaged:", {
@ -490,7 +527,7 @@ export function findExportedComponentLazy<T extends object = any>(...props: Prop
* closeModal: filters.byCode("key==") * closeModal: filters.byCode("key==")
* }) * })
*/ */
export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>): Record<S, any> { export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>, includeBlacklistedExports = false): Record<S, any> {
const exports = {} as Record<S, any>; const exports = {} as Record<S, any>;
const id = findModuleId(...Array.isArray(code) ? code : [code]); const id = findModuleId(...Array.isArray(code) ? code : [code]);
@ -498,8 +535,9 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
return exports; return exports;
const mod = wreq(id as any); const mod = wreq(id as any);
const keys = getAllPropertyNames(mod, includeBlacklistedExports);
outer: outer:
for (const key in mod) { for (const key of keys) {
const member = mod[key]; const member = mod[key];
for (const newName in mappers) { for (const newName in mappers) {
// if the current mapper matches this module // if the current mapper matches this module
@ -513,24 +551,13 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
}); });
/** /**
* {@link mapMangledModule}, lazy. * lazy mapMangledModule
* @see {@link mapMangledModule}
* Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module)
* then maps it into an easily usable module via the specified mappers.
*
* @param code The code to look for
* @param mappers Mappers to create the non mangled exports
* @returns Unmangled exports as specified in mappers
*
* @example mapMangledModule("headerIdIsManaged:", {
* openModal: filters.byCode("headerIdIsManaged:"),
* closeModal: filters.byCode("key==")
* })
*/ */
export function mapMangledModuleLazy<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>): Record<S, any> { export function mapMangledModuleLazy<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>, includeBlacklistedExports = false): Record<S, any> {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers, includeBlacklistedExports]]);
return proxyLazy(() => mapMangledModule(code, mappers)); return proxyLazy(() => mapMangledModule(code, mappers, includeBlacklistedExports));
} }
export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/; export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/;

93
src/webpack/wreq.d.ts vendored
View file

@ -17,14 +17,11 @@ export type Module = {
/** exports can be anything, however initially it is always an empty object */ /** exports can be anything, however initially it is always an empty object */
export type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void; export type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void;
export type WebpackQueues = unique symbol | "__webpack_queues__"; /** Keys here can be symbols too, but we can't properly type them */
export type WebpackExports = unique symbol | "__webpack_exports__";
export type WebpackError = unique symbol | "__webpack_error__";
export type AsyncModulePromise = Promise<ModuleExports> & { export type AsyncModulePromise = Promise<ModuleExports> & {
[WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any; "__webpack_queues__": (fnQueue: ((queue: any[]) => any)) => any;
[WebpackExports]: ModuleExports; "__webpack_exports__": ModuleExports;
[WebpackError]?: any; "__webpack_error__"?: any;
}; };
export type AsyncModuleBody = ( export type AsyncModuleBody = (
@ -33,27 +30,45 @@ export type AsyncModuleBody = (
asyncResult: (error?: any) => void asyncResult: (error?: any) => void
) => Promise<void>; ) => Promise<void>;
export type ChunkHandlers = { export type EnsureChunkHandlers = {
/** /**
* Ensures the js file for this chunk is loaded, or starts to load if it's not. * Ensures the js file for this chunk is loaded, or starts to load if it's not.
* @param chunkId The chunk id * @param chunkId The chunk id
* @param promises The promises array to add the loading promise to * @param promises The promises array to add the loading promise to
*/ */
j: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void, j: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void;
/** /**
* Ensures the css file for this chunk is loaded, or starts to load if it's not. * Ensures the css file for this chunk is loaded, or starts to load if it's not.
* @param chunkId The chunk id * @param chunkId The chunk id
* @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too * @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too
*/ */
css: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void, css: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void;
/**
* Trigger for prefetching next chunks. This is called after ensuring a chunk is loaded and internally looks up
* a map to see if the chunk that just loaded has next chunks to prefetch.
*
* Note that this does not add an extra promise to the promises array, and instead only executes the prefetching after
* calling Promise.all on the promises array.
* @param chunkId The chunk id
* @param promises The promises array of ensuring the chunk is loaded
*/
prefetch: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void;
};
export type PrefetchChunkHandlers = {
/**
* Prefetches the js file for this chunk.
* @param chunkId The chunk id
*/
j: (this: PrefetchChunkHandlers, chunkId: PropertyKey) => void;
}; };
export type ScriptLoadDone = (event: Event) => void; export type ScriptLoadDone = (event: Event) => void;
// export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & { export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & {
// /** Check if a chunk has been loaded */ /** Check if a chunk has been loaded */
// j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean; j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean;
// }; };
export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {
/** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
@ -134,14 +149,21 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {
* } * }
* // exports is now { exportName: someExportedValue } (but each value is actually a getter) * // exports is now { exportName: someExportedValue } (but each value is actually a getter)
*/ */
d: (this: WebpackRequire, exports: AnyRecord, definiton: AnyRecord) => void; d: (this: WebpackRequire, exports: Record<PropertyKey, any>, definiton: Record<PropertyKey, () => ModuleExports>) => void;
/** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */ /** The ensure chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */
f: ChunkHandlers; f: EnsureChunkHandlers;
/** /**
* The ensure chunk function, it ensures a chunk is loaded, or loads if needed. * The ensure chunk function, it ensures a chunk is loaded, or loads if needed.
* Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded. * Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded.
*/ */
e: (this: WebpackRequire, chunkId: PropertyKey) => Promise<void[]>; e: (this: WebpackRequire, chunkId: PropertyKey) => Promise<void[]>;
/** The prefetch chunk handlers, which are used to prefetch the files of the chunks */
F: PrefetchChunkHandlers;
/**
* The prefetch chunk function.
* Internally it uses the handlers in {@link WebpackRequire.F} to prefetch a chunk.
*/
E: (this: WebpackRequire, chunkId: PropertyKey) => void;
/** Get the filename for the css part of a chunk */ /** Get the filename for the css part of a chunk */
k: (this: WebpackRequire, chunkId: PropertyKey) => string; k: (this: WebpackRequire, chunkId: PropertyKey) => string;
/** Get the filename for the js part of a chunk */ /** Get the filename for the js part of a chunk */
@ -162,18 +184,18 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {
r: (this: WebpackRequire, exports: ModuleExports) => void; r: (this: WebpackRequire, exports: ModuleExports) => void;
/** Node.js module decorator. Decorates a module as a Node.js module */ /** Node.js module decorator. Decorates a module as a Node.js module */
nmd: (this: WebpackRequire, module: Module) => any; nmd: (this: WebpackRequire, module: Module) => any;
// /** /**
// * Register deferred code which will be executed when the passed chunks are loaded. * Register deferred code which will be executed when the passed chunks are loaded.
// * *
// * If chunkIds is defined, it defers the execution of the callback and returns undefined. * If chunkIds is defined, it defers the execution of the callback and returns undefined.
// * *
// * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument. * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument.
// * *
// * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code. * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code.
// * *
// * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed. * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed.
// */ */
// O: OnChunksLoaded; O: OnChunksLoaded;
/** /**
* Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports". * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports".
* @returns The exports argument, but now assigned with the exports of the wasm instance * @returns The exports argument, but now assigned with the exports of the wasm instance
@ -185,6 +207,13 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {
j: string; j: string;
/** Document baseURI or WebWorker location.href */ /** Document baseURI or WebWorker location.href */
b: string; b: string;
/* rspack only */
/** rspack version */
rv: (this: WebpackRequire) => string;
/** rspack unique id */
ruid: string;
}; };
// Utility section for Vencord // Utility section for Vencord
@ -200,12 +229,10 @@ export type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: M
[SYM_PATCHED_BY]?: Set<string>; [SYM_PATCHED_BY]?: Set<string>;
}; };
export type WrappedModuleFactory = AnyModuleFactory & { export type PatchedModuleFactory = AnyModuleFactory & {
[SYM_ORIGINAL_FACTORY]: AnyModuleFactory; [SYM_ORIGINAL_FACTORY]: AnyModuleFactory;
[SYM_PATCHED_SOURCE]?: string; [SYM_PATCHED_SOURCE]?: string;
[SYM_PATCHED_BY]?: Set<string>; [SYM_PATCHED_BY]?: Set<string>;
}; };
export type MaybeWrappedModuleFactory = AnyModuleFactory | WrappedModuleFactory; export type MaybePatchedModuleFactory = PatchedModuleFactory | AnyModuleFactory;
export type WrappedModuleFactories = Record<PropertyKey, WrappedModuleFactory>;

View file

@ -29,7 +29,9 @@
"@shared/*": ["./shared/*"], "@shared/*": ["./shared/*"],
"@webpack/types": ["./webpack/common/types"], "@webpack/types": ["./webpack/common/types"],
"@webpack/common": ["./webpack/common"], "@webpack/common": ["./webpack/common"],
"@webpack": ["./webpack/webpack"] "@webpack": ["./webpack/webpack"],
"@webpack/patcher": ["./webpack/patchWebpack"],
"@webpack/wreq.d": ["./webpack/wreq.d"],
}, },
"plugins": [ "plugins": [