diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 875f3568..79197bea 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -44,7 +44,7 @@ jobs:
- name: Clean up obsolete files
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
id: release_values
diff --git a/.gitignore b/.gitignore
index 5440aa5f..5df31e1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ vencord_installer
.DS_Store
yarn.lock
+bun.lock
package-lock.json
*.log
diff --git a/browser/manifest.json b/browser/manifest.json
index c30ddf2d..300705ae 100644
--- a/browser/manifest.json
+++ b/browser/manifest.json
@@ -36,7 +36,7 @@
"web_accessible_resources": [
{
- "resources": ["dist/*", "third-party/*"],
+ "resources": ["dist/*", "vendor/*"],
"matches": ["*://*.discord.com/*"]
}
],
diff --git a/browser/monaco.ts b/browser/monaco.ts
index ead061d6..dc243df7 100644
--- a/browser/monaco.ts
+++ b/browser/monaco.ts
@@ -15,7 +15,7 @@ declare global {
const getTheme: () => string;
}
-const BASE = "/dist/monaco/vs";
+const BASE = "/vendor/monaco/vs";
self.MonacoEnvironment = {
getWorkerUrl(_moduleId: unknown, label: string) {
diff --git a/browser/monacoWin.html b/browser/monacoWin.html
index a55b0e54..12523d45 100644
--- a/browser/monacoWin.html
+++ b/browser/monacoWin.html
@@ -24,12 +24,12 @@
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 67327b93..d59c3753 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -134,7 +134,7 @@ export default tseslint.config(
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
- "prefer-const": "error",
+ "prefer-const": ["error", { destructuring: "all" }],
"prefer-spread": "error",
// Plugin Rules
diff --git a/package.json b/package.json
index b9e9c0a8..ce3ce55a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
- "version": "1.11.4",
+ "version": "1.11.5",
"description": "Vencord+ is a fork of Vencord that adds unapproved plugins.",
"homepage": "https://github.com/RobinRMC/VencordPlus#readme",
"bugs": {
@@ -33,13 +33,12 @@
"lint": "eslint",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix",
- "test": "pnpm buildStandalone && pnpm testTsc && pnpm generatePluginJson",
+ "test": "pnpm buildStandalone && pnpm testTsc && pnpm lint && pnpm lint-styles && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit"
},
"dependencies": {
"@intrnl/xxhash64": "^0.1.2",
- "@sapphi-red/web-noise-suppressor": "0.3.5",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.5",
"fflate": "^0.8.2",
@@ -59,7 +58,7 @@
"@types/yazl": "^2.4.5",
"diff": "^7.0.0",
"discord-types": "^1.3.26",
- "esbuild": "^0.15.18",
+ "esbuild": "^0.25.0",
"eslint": "^9.17.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "2.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index beff03d1..ec65cc1b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,9 +19,6 @@ importers:
'@intrnl/xxhash64':
specifier: ^0.1.2
version: 0.1.2
- '@sapphi-red/web-noise-suppressor':
- specifier: 0.3.5
- version: 0.3.5
'@vap/core':
specifier: 0.0.12
version: 0.0.12
@@ -75,8 +72,8 @@ importers:
specifier: ^1.3.26
version: 1.3.26
esbuild:
- specifier: ^0.15.18
- version: 0.15.18
+ specifier: ^0.25.0
+ version: 0.25.0
eslint:
specifier: ^9.17.0
version: 9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
@@ -232,6 +229,12 @@ packages:
cpu: [ppc64]
os: [aix]
+ '@esbuild/aix-ppc64@0.25.0':
+ resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
'@esbuild/android-arm64@0.17.19':
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
engines: {node: '>=12'}
@@ -244,10 +247,10 @@ packages:
cpu: [arm64]
os: [android]
- '@esbuild/android-arm@0.15.18':
- resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
- engines: {node: '>=12'}
- cpu: [arm]
+ '@esbuild/android-arm64@0.25.0':
+ resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.17.19':
@@ -262,6 +265,12 @@ packages:
cpu: [arm]
os: [android]
+ '@esbuild/android-arm@0.25.0':
+ resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
'@esbuild/android-x64@0.17.19':
resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
engines: {node: '>=12'}
@@ -274,6 +283,12 @@ packages:
cpu: [x64]
os: [android]
+ '@esbuild/android-x64@0.25.0':
+ resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
'@esbuild/darwin-arm64@0.17.19':
resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
engines: {node: '>=12'}
@@ -286,6 +301,12 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@esbuild/darwin-arm64@0.25.0':
+ resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
'@esbuild/darwin-x64@0.17.19':
resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
engines: {node: '>=12'}
@@ -298,6 +319,12 @@ packages:
cpu: [x64]
os: [darwin]
+ '@esbuild/darwin-x64@0.25.0':
+ resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
'@esbuild/freebsd-arm64@0.17.19':
resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
engines: {node: '>=12'}
@@ -310,6 +337,12 @@ packages:
cpu: [arm64]
os: [freebsd]
+ '@esbuild/freebsd-arm64@0.25.0':
+ resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
'@esbuild/freebsd-x64@0.17.19':
resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
engines: {node: '>=12'}
@@ -322,6 +355,12 @@ packages:
cpu: [x64]
os: [freebsd]
+ '@esbuild/freebsd-x64@0.25.0':
+ resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
'@esbuild/linux-arm64@0.17.19':
resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
engines: {node: '>=12'}
@@ -334,6 +373,12 @@ packages:
cpu: [arm64]
os: [linux]
+ '@esbuild/linux-arm64@0.25.0':
+ resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
'@esbuild/linux-arm@0.17.19':
resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
engines: {node: '>=12'}
@@ -346,6 +391,12 @@ packages:
cpu: [arm]
os: [linux]
+ '@esbuild/linux-arm@0.25.0':
+ resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
'@esbuild/linux-ia32@0.17.19':
resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
engines: {node: '>=12'}
@@ -358,10 +409,10 @@ packages:
cpu: [ia32]
os: [linux]
- '@esbuild/linux-loong64@0.15.18':
- resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==}
- engines: {node: '>=12'}
- cpu: [loong64]
+ '@esbuild/linux-ia32@0.25.0':
+ resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.17.19':
@@ -376,6 +427,12 @@ packages:
cpu: [loong64]
os: [linux]
+ '@esbuild/linux-loong64@0.25.0':
+ resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
'@esbuild/linux-mips64el@0.17.19':
resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
engines: {node: '>=12'}
@@ -388,6 +445,12 @@ packages:
cpu: [mips64el]
os: [linux]
+ '@esbuild/linux-mips64el@0.25.0':
+ resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
'@esbuild/linux-ppc64@0.17.19':
resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
engines: {node: '>=12'}
@@ -400,6 +463,12 @@ packages:
cpu: [ppc64]
os: [linux]
+ '@esbuild/linux-ppc64@0.25.0':
+ resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
'@esbuild/linux-riscv64@0.17.19':
resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
engines: {node: '>=12'}
@@ -412,6 +481,12 @@ packages:
cpu: [riscv64]
os: [linux]
+ '@esbuild/linux-riscv64@0.25.0':
+ resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
'@esbuild/linux-s390x@0.17.19':
resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
engines: {node: '>=12'}
@@ -424,6 +499,12 @@ packages:
cpu: [s390x]
os: [linux]
+ '@esbuild/linux-s390x@0.25.0':
+ resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
'@esbuild/linux-x64@0.17.19':
resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
engines: {node: '>=12'}
@@ -436,6 +517,18 @@ packages:
cpu: [x64]
os: [linux]
+ '@esbuild/linux-x64@0.25.0':
+ resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.0':
+ resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
'@esbuild/netbsd-x64@0.17.19':
resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
engines: {node: '>=12'}
@@ -448,12 +541,24 @@ packages:
cpu: [x64]
os: [netbsd]
+ '@esbuild/netbsd-x64@0.25.0':
+ resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
'@esbuild/openbsd-arm64@0.23.1':
resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
+ '@esbuild/openbsd-arm64@0.25.0':
+ resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
'@esbuild/openbsd-x64@0.17.19':
resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
engines: {node: '>=12'}
@@ -466,6 +571,12 @@ packages:
cpu: [x64]
os: [openbsd]
+ '@esbuild/openbsd-x64@0.25.0':
+ resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
'@esbuild/sunos-x64@0.17.19':
resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
engines: {node: '>=12'}
@@ -478,6 +589,12 @@ packages:
cpu: [x64]
os: [sunos]
+ '@esbuild/sunos-x64@0.25.0':
+ resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
'@esbuild/win32-arm64@0.17.19':
resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
engines: {node: '>=12'}
@@ -490,6 +607,12 @@ packages:
cpu: [arm64]
os: [win32]
+ '@esbuild/win32-arm64@0.25.0':
+ resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
'@esbuild/win32-ia32@0.17.19':
resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
engines: {node: '>=12'}
@@ -502,6 +625,12 @@ packages:
cpu: [ia32]
os: [win32]
+ '@esbuild/win32-ia32@0.25.0':
+ resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
'@esbuild/win32-x64@0.17.19':
resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
engines: {node: '>=12'}
@@ -514,6 +643,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@esbuild/win32-x64@0.25.0':
+ resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@eslint-community/eslint-utils@4.4.1':
resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -612,9 +747,6 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
- '@sapphi-red/web-noise-suppressor@0.3.5':
- resolution: {integrity: sha512-jh3+V9yM+zxLriQexoGm0GatoPaJWjs6ypFIbFYwQp+AoUb55eUXrjKtKQyuC5zShzzeAQUl0M5JzqB7SSrsRA==}
-
'@stylistic/eslint-plugin@2.12.1':
resolution: {integrity: sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1208,131 +1340,6 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
- esbuild-android-64@0.15.18:
- resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [android]
-
- esbuild-android-arm64@0.15.18:
- resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [android]
-
- esbuild-darwin-64@0.15.18:
- resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [darwin]
-
- esbuild-darwin-arm64@0.15.18:
- resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [darwin]
-
- esbuild-freebsd-64@0.15.18:
- resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [freebsd]
-
- esbuild-freebsd-arm64@0.15.18:
- resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [freebsd]
-
- esbuild-linux-32@0.15.18:
- resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [linux]
-
- esbuild-linux-64@0.15.18:
- resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [linux]
-
- esbuild-linux-arm64@0.15.18:
- resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [linux]
-
- esbuild-linux-arm@0.15.18:
- resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==}
- engines: {node: '>=12'}
- cpu: [arm]
- os: [linux]
-
- esbuild-linux-mips64le@0.15.18:
- resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==}
- engines: {node: '>=12'}
- cpu: [mips64el]
- os: [linux]
-
- esbuild-linux-ppc64le@0.15.18:
- resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==}
- engines: {node: '>=12'}
- cpu: [ppc64]
- os: [linux]
-
- esbuild-linux-riscv64@0.15.18:
- resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==}
- engines: {node: '>=12'}
- cpu: [riscv64]
- os: [linux]
-
- esbuild-linux-s390x@0.15.18:
- resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==}
- engines: {node: '>=12'}
- cpu: [s390x]
- os: [linux]
-
- esbuild-netbsd-64@0.15.18:
- resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [netbsd]
-
- esbuild-openbsd-64@0.15.18:
- resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [openbsd]
-
- esbuild-sunos-64@0.15.18:
- resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [sunos]
-
- esbuild-windows-32@0.15.18:
- resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==}
- engines: {node: '>=12'}
- cpu: [ia32]
- os: [win32]
-
- esbuild-windows-64@0.15.18:
- resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==}
- engines: {node: '>=12'}
- cpu: [x64]
- os: [win32]
-
- esbuild-windows-arm64@0.15.18:
- resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==}
- engines: {node: '>=12'}
- cpu: [arm64]
- os: [win32]
-
- esbuild@0.15.18:
- resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==}
- engines: {node: '>=12'}
- hasBin: true
-
esbuild@0.17.19:
resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
engines: {node: '>=12'}
@@ -1343,6 +1350,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ esbuild@0.25.0:
+ resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -2898,13 +2910,16 @@ snapshots:
'@esbuild/aix-ppc64@0.23.1':
optional: true
+ '@esbuild/aix-ppc64@0.25.0':
+ optional: true
+
'@esbuild/android-arm64@0.17.19':
optional: true
'@esbuild/android-arm64@0.23.1':
optional: true
- '@esbuild/android-arm@0.15.18':
+ '@esbuild/android-arm64@0.25.0':
optional: true
'@esbuild/android-arm@0.17.19':
@@ -2913,55 +2928,79 @@ snapshots:
'@esbuild/android-arm@0.23.1':
optional: true
+ '@esbuild/android-arm@0.25.0':
+ optional: true
+
'@esbuild/android-x64@0.17.19':
optional: true
'@esbuild/android-x64@0.23.1':
optional: true
+ '@esbuild/android-x64@0.25.0':
+ optional: true
+
'@esbuild/darwin-arm64@0.17.19':
optional: true
'@esbuild/darwin-arm64@0.23.1':
optional: true
+ '@esbuild/darwin-arm64@0.25.0':
+ optional: true
+
'@esbuild/darwin-x64@0.17.19':
optional: true
'@esbuild/darwin-x64@0.23.1':
optional: true
+ '@esbuild/darwin-x64@0.25.0':
+ optional: true
+
'@esbuild/freebsd-arm64@0.17.19':
optional: true
'@esbuild/freebsd-arm64@0.23.1':
optional: true
+ '@esbuild/freebsd-arm64@0.25.0':
+ optional: true
+
'@esbuild/freebsd-x64@0.17.19':
optional: true
'@esbuild/freebsd-x64@0.23.1':
optional: true
+ '@esbuild/freebsd-x64@0.25.0':
+ optional: true
+
'@esbuild/linux-arm64@0.17.19':
optional: true
'@esbuild/linux-arm64@0.23.1':
optional: true
+ '@esbuild/linux-arm64@0.25.0':
+ optional: true
+
'@esbuild/linux-arm@0.17.19':
optional: true
'@esbuild/linux-arm@0.23.1':
optional: true
+ '@esbuild/linux-arm@0.25.0':
+ optional: true
+
'@esbuild/linux-ia32@0.17.19':
optional: true
'@esbuild/linux-ia32@0.23.1':
optional: true
- '@esbuild/linux-loong64@0.15.18':
+ '@esbuild/linux-ia32@0.25.0':
optional: true
'@esbuild/linux-loong64@0.17.19':
@@ -2970,75 +3009,117 @@ snapshots:
'@esbuild/linux-loong64@0.23.1':
optional: true
+ '@esbuild/linux-loong64@0.25.0':
+ optional: true
+
'@esbuild/linux-mips64el@0.17.19':
optional: true
'@esbuild/linux-mips64el@0.23.1':
optional: true
+ '@esbuild/linux-mips64el@0.25.0':
+ optional: true
+
'@esbuild/linux-ppc64@0.17.19':
optional: true
'@esbuild/linux-ppc64@0.23.1':
optional: true
+ '@esbuild/linux-ppc64@0.25.0':
+ optional: true
+
'@esbuild/linux-riscv64@0.17.19':
optional: true
'@esbuild/linux-riscv64@0.23.1':
optional: true
+ '@esbuild/linux-riscv64@0.25.0':
+ optional: true
+
'@esbuild/linux-s390x@0.17.19':
optional: true
'@esbuild/linux-s390x@0.23.1':
optional: true
+ '@esbuild/linux-s390x@0.25.0':
+ optional: true
+
'@esbuild/linux-x64@0.17.19':
optional: true
'@esbuild/linux-x64@0.23.1':
optional: true
+ '@esbuild/linux-x64@0.25.0':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.0':
+ optional: true
+
'@esbuild/netbsd-x64@0.17.19':
optional: true
'@esbuild/netbsd-x64@0.23.1':
optional: true
+ '@esbuild/netbsd-x64@0.25.0':
+ optional: true
+
'@esbuild/openbsd-arm64@0.23.1':
optional: true
+ '@esbuild/openbsd-arm64@0.25.0':
+ optional: true
+
'@esbuild/openbsd-x64@0.17.19':
optional: true
'@esbuild/openbsd-x64@0.23.1':
optional: true
+ '@esbuild/openbsd-x64@0.25.0':
+ optional: true
+
'@esbuild/sunos-x64@0.17.19':
optional: true
'@esbuild/sunos-x64@0.23.1':
optional: true
+ '@esbuild/sunos-x64@0.25.0':
+ optional: true
+
'@esbuild/win32-arm64@0.17.19':
optional: true
'@esbuild/win32-arm64@0.23.1':
optional: true
+ '@esbuild/win32-arm64@0.25.0':
+ optional: true
+
'@esbuild/win32-ia32@0.17.19':
optional: true
'@esbuild/win32-ia32@0.23.1':
optional: true
+ '@esbuild/win32-ia32@0.25.0':
+ optional: true
+
'@esbuild/win32-x64@0.17.19':
optional: true
'@esbuild/win32-x64@0.23.1':
optional: true
+ '@esbuild/win32-x64@0.25.0':
+ optional: true
+
'@eslint-community/eslint-utils@4.4.1(eslint@9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))':
dependencies:
eslint: 9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)
@@ -3144,8 +3225,6 @@ snapshots:
'@rtsao/scc@1.1.0': {}
- '@sapphi-red/web-noise-suppressor@0.3.5': {}
-
'@stylistic/eslint-plugin@2.12.1(eslint@9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.2)':
dependencies:
'@typescript-eslint/utils': 8.18.1(eslint@9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.2)
@@ -3945,91 +4024,6 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
- esbuild-android-64@0.15.18:
- optional: true
-
- esbuild-android-arm64@0.15.18:
- optional: true
-
- esbuild-darwin-64@0.15.18:
- optional: true
-
- esbuild-darwin-arm64@0.15.18:
- optional: true
-
- esbuild-freebsd-64@0.15.18:
- optional: true
-
- esbuild-freebsd-arm64@0.15.18:
- optional: true
-
- esbuild-linux-32@0.15.18:
- optional: true
-
- esbuild-linux-64@0.15.18:
- optional: true
-
- esbuild-linux-arm64@0.15.18:
- optional: true
-
- esbuild-linux-arm@0.15.18:
- optional: true
-
- esbuild-linux-mips64le@0.15.18:
- optional: true
-
- esbuild-linux-ppc64le@0.15.18:
- optional: true
-
- esbuild-linux-riscv64@0.15.18:
- optional: true
-
- esbuild-linux-s390x@0.15.18:
- optional: true
-
- esbuild-netbsd-64@0.15.18:
- optional: true
-
- esbuild-openbsd-64@0.15.18:
- optional: true
-
- esbuild-sunos-64@0.15.18:
- optional: true
-
- esbuild-windows-32@0.15.18:
- optional: true
-
- esbuild-windows-64@0.15.18:
- optional: true
-
- esbuild-windows-arm64@0.15.18:
- optional: true
-
- esbuild@0.15.18:
- optionalDependencies:
- '@esbuild/android-arm': 0.15.18
- '@esbuild/linux-loong64': 0.15.18
- esbuild-android-64: 0.15.18
- esbuild-android-arm64: 0.15.18
- esbuild-darwin-64: 0.15.18
- esbuild-darwin-arm64: 0.15.18
- esbuild-freebsd-64: 0.15.18
- esbuild-freebsd-arm64: 0.15.18
- esbuild-linux-32: 0.15.18
- esbuild-linux-64: 0.15.18
- esbuild-linux-arm: 0.15.18
- esbuild-linux-arm64: 0.15.18
- esbuild-linux-mips64le: 0.15.18
- esbuild-linux-ppc64le: 0.15.18
- esbuild-linux-riscv64: 0.15.18
- esbuild-linux-s390x: 0.15.18
- esbuild-netbsd-64: 0.15.18
- esbuild-openbsd-64: 0.15.18
- esbuild-sunos-64: 0.15.18
- esbuild-windows-32: 0.15.18
- esbuild-windows-64: 0.15.18
- esbuild-windows-arm64: 0.15.18
-
esbuild@0.17.19:
optionalDependencies:
'@esbuild/android-arm': 0.17.19
@@ -4082,6 +4076,34 @@ snapshots:
'@esbuild/win32-ia32': 0.23.1
'@esbuild/win32-x64': 0.23.1
+ esbuild@0.25.0:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.0
+ '@esbuild/android-arm': 0.25.0
+ '@esbuild/android-arm64': 0.25.0
+ '@esbuild/android-x64': 0.25.0
+ '@esbuild/darwin-arm64': 0.25.0
+ '@esbuild/darwin-x64': 0.25.0
+ '@esbuild/freebsd-arm64': 0.25.0
+ '@esbuild/freebsd-x64': 0.25.0
+ '@esbuild/linux-arm': 0.25.0
+ '@esbuild/linux-arm64': 0.25.0
+ '@esbuild/linux-ia32': 0.25.0
+ '@esbuild/linux-loong64': 0.25.0
+ '@esbuild/linux-mips64el': 0.25.0
+ '@esbuild/linux-ppc64': 0.25.0
+ '@esbuild/linux-riscv64': 0.25.0
+ '@esbuild/linux-s390x': 0.25.0
+ '@esbuild/linux-x64': 0.25.0
+ '@esbuild/netbsd-arm64': 0.25.0
+ '@esbuild/netbsd-x64': 0.25.0
+ '@esbuild/openbsd-arm64': 0.25.0
+ '@esbuild/openbsd-x64': 0.25.0
+ '@esbuild/sunos-x64': 0.25.0
+ '@esbuild/win32-arm64': 0.25.0
+ '@esbuild/win32-ia32': 0.25.0
+ '@esbuild/win32-x64': 0.25.0
+
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs
index 241710f1..692af16d 100755
--- a/scripts/build/build.mjs
+++ b/scripts/build/build.mjs
@@ -17,38 +17,41 @@
* along with this program. If not, see .
*/
-import esbuild from "esbuild";
+// @ts-check
+
import { readdir } from "fs/promises";
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_DEV,
IS_REPORTER,
IS_UPDATER_DISABLED,
IS_WEB: false,
IS_EXTENSION: false,
- VERSION: JSON.stringify(VERSION),
+ VERSION,
BUILD_TIMESTAMP
-};
+});
-if (defines.IS_STANDALONE === false)
+if (defines.IS_STANDALONE === "false") {
// If this is a local build (not standalone), optimize
// for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform);
+}
/**
- * @type {esbuild.BuildOptions}
+ * @type {import("esbuild").BuildOptions}
*/
const nodeCommonOpts = {
...commonOpts,
+ define: defines,
format: "cjs",
platform: "node",
target: ["esnext"],
- external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
- define: defines
+ // @ts-ignore this is never undefined
+ external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external]
};
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
- esbuild.build({
+ {
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
- define: {
- ...defines,
- IS_DISCORD_DESKTOP: true,
- IS_VESKTOP: false
- },
plugins: [
+ // @ts-ignore this is never undefined
...nodeCommonOpts.plugins,
globNativesPlugin
- ]
- }),
- esbuild.build({
+ ],
+ define: {
+ ...defines,
+ IS_DISCORD_DESKTOP: "true",
+ IS_VESKTOP: "false"
+ }
+ },
+ {
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js",
@@ -135,11 +140,11 @@ await Promise.all([
],
define: {
...defines,
- IS_DISCORD_DESKTOP: true,
- IS_VESKTOP: false
+ IS_DISCORD_DESKTOP: "true",
+ IS_VESKTOP: "false"
}
- }),
- esbuild.build({
+ },
+ {
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
@@ -147,29 +152,29 @@ await Promise.all([
sourcemap,
define: {
...defines,
- IS_DISCORD_DESKTOP: true,
- IS_VESKTOP: false
+ IS_DISCORD_DESKTOP: "true",
+ IS_VESKTOP: "false"
}
- }),
+ },
// Vencord Desktop main & renderer & preload
- esbuild.build({
+ {
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap,
- define: {
- ...defines,
- IS_DISCORD_DESKTOP: false,
- IS_VESKTOP: true
- },
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
- ]
- }),
- esbuild.build({
+ ],
+ define: {
+ ...defines,
+ IS_DISCORD_DESKTOP: "false",
+ IS_VESKTOP: "true"
+ }
+ },
+ {
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js",
@@ -184,11 +189,11 @@ await Promise.all([
],
define: {
...defines,
- IS_DISCORD_DESKTOP: false,
- IS_VESKTOP: true
+ IS_DISCORD_DESKTOP: "false",
+ IS_VESKTOP: "true"
}
- }),
- esbuild.build({
+ },
+ {
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/vencordDesktopPreload.js",
@@ -196,14 +201,10 @@ await Promise.all([
sourcemap,
define: {
...defines,
- IS_DISCORD_DESKTOP: false,
- IS_VESKTOP: true
+ IS_DISCORD_DESKTOP: "false",
+ IS_VESKTOP: "true"
}
- }),
-]).catch(err => {
- console.error("Build failed");
- console.error(err.message);
- // make ci fail
- if (!commonOpts.watch)
- process.exitCode = 1;
-});
+ }
+]);
+
+await buildOrWatchAll(buildConfigs);
diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs
index deab8661..33168ff9 100644
--- a/scripts/build/buildWeb.mjs
+++ b/scripts/build/buildWeb.mjs
@@ -17,29 +17,30 @@
* along with this program. If not, see .
*/
-import esbuild from "esbuild";
+// @ts-check
+
import { readFileSync } from "fs";
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path";
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 = {
...commonOpts,
entryPoints: ["browser/Vencord.ts"],
- globalName: "Vencord",
format: "iife",
+ globalName: "Vencord",
external: ["~plugins", "~git-hash", "/assets/*"],
+ target: ["esnext"],
plugins: [
globPlugins("web"),
...commonRendererPlugins
],
- target: ["esnext"],
- define: {
+ define: stringifyValues({
IS_WEB: true,
IS_EXTENSION: false,
IS_STANDALONE: true,
@@ -48,9 +49,9 @@ const commonOptions = {
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: false,
IS_UPDATER_DISABLED: true,
- VERSION: JSON.stringify(VERSION),
+ VERSION,
BUILD_TIMESTAMP
- }
+ })
};
const MonacoWorkerEntryPoints = [
@@ -58,70 +59,59 @@ const MonacoWorkerEntryPoints = [
"vs/editor/editor.worker.js"
];
-const RnNoiseFiles = [
- "dist/rnnoise.wasm",
- "dist/rnnoise_simd.wasm",
- "dist/rnnoise/workletProcessor.js",
- "LICENSE"
+/** @type {import("esbuild").BuildOptions[]} */
+const buildConfigs = [
+ {
+ entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
+ 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(
- [
- 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);
-});;
+await buildOrWatchAll(buildConfigs);
/**
* @type {(dir: string) => Promise}
@@ -155,16 +145,13 @@ async function buildExtension(target, files) {
const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"),
- ...await loadDir("dist/monaco"),
- ...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}`)]
- ))),
+ ...await loadDir("dist/vendor/monaco", "dist/"),
...Object.fromEntries(await Promise.all(files.map(async f => {
let content = await readFile(join("browser", f));
if (f.startsWith("manifest")) {
const json = JSON.parse(content.toString("utf-8"));
json.version = VERSION;
- content = new TextEncoder().encode(JSON.stringify(json));
+ content = Buffer.from(new TextEncoder().encode(JSON.stringify(json)));
}
return [
@@ -210,7 +197,6 @@ if (!process.argv.includes("--skip-extension")) {
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
-
} else {
await appendCssRuntime;
}
diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs
index ff4b3b8a..301ddc9b 100644
--- a/scripts/build/common.mjs
+++ b/scripts/build/common.mjs
@@ -16,11 +16,13 @@
* along with this program. If not, see .
*/
+// @ts-check
+
import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js";
import { exec, execSync } from "child_process";
-import esbuild from "esbuild";
+import esbuild, { build, context } from "esbuild";
import { constants as FsConstants, readFileSync } from "fs";
import { access, readdir, readFile } from "fs/promises";
import { minify as minifyHtml } from "html-minifier-terser";
@@ -31,7 +33,7 @@ import { getPluginTarget } from "../utils.mjs";
import { builtinModules } from "module";
/** @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;
// https://reproducible-builds.org/docs/source-date-epoch/
@@ -54,6 +56,34 @@ export const banner = {
`.trim()
};
+/**
+ * JSON.stringify all values in an object
+ * @type {(obj: Record) => Record}
+ */
+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/;
/**
* @param {string} base
@@ -311,18 +341,16 @@ export const banImportPlugin = (filter, message) => ({
export const commonOpts = {
logLevel: "info",
bundle: true,
- watch,
- minify: !watch,
- sourcemap: watch ? "inline" : "",
+ minify: !watch && !IS_REPORTER,
+ sourcemap: watch ? "inline" : "external",
legalComments: "linked",
banner,
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
+ jsx: "transform",
jsxFactory: "VencordCreateElement",
- jsxFragment: "VencordFragment",
- // Work around https://github.com/evanw/esbuild/issues/2460
- tsconfig: "./scripts/build/tsconfig.esbuild.json"
+ jsxFragment: "VencordFragment"
};
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(/^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"),
+ // @ts-ignore this is never undefined
...commonOpts.plugins
];
diff --git a/scripts/build/tsconfig.esbuild.json b/scripts/build/tsconfig.esbuild.json
deleted file mode 100644
index e3e28a14..00000000
--- a/scripts/build/tsconfig.esbuild.json
+++ /dev/null
@@ -1,7 +0,0 @@
-// Work around https://github.com/evanw/esbuild/issues/2460
-{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "jsx": "react"
- }
-}
diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts
index f4d5bbbc..26daf262 100644
--- a/scripts/generateReport.ts
+++ b/scripts/generateReport.ts
@@ -16,24 +16,27 @@
* along with this program. If not, see .
*/
-/* eslint-disable no-fallthrough */
-
-// eslint-disable-next-line spaced-comment
///
-// eslint-disable-next-line spaced-comment
///
+import { createHmac } from "crypto";
import { readFileSync } from "fs";
import pup, { JSHandle } from "puppeteer-core";
-for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
+const logStderr = (...data: any[]) => console.error(`${CANARY ? "CANARY" : "STABLE"} ---`, ...data);
+
+for (const variable of ["CHROMIUM_BIN"]) {
if (!process.env[variable]) {
- console.error(`Missing environment variable ${variable}`);
+ logStderr(`Missing environment variable ${variable}`);
process.exit(1);
}
}
const CANARY = process.env.USE_CANARY === "true";
+let metaData = {
+ buildNumber: "Unknown Build Number",
+ buildHash: "Unknown Build Hash"
+};
const browser = await pup.launch({
headless: true,
@@ -51,14 +54,17 @@ async function maybeGetError(handle: JSHandle): Promise {
.catch(() => undefined);
}
+interface PatchInfo {
+ plugin: string;
+ type: string;
+ id: string;
+ match: string;
+ error?: string;
+};
+
const report = {
- badPatches: [] as {
- plugin: string;
- type: string;
- id: string;
- match: string;
- error?: string;
- }[],
+ badPatches: [] as PatchInfo[],
+ slowPatches: [] as PatchInfo[],
badStarts: [] as {
plugin: string;
error: string;
@@ -128,56 +134,88 @@ async function printReport() {
console.log();
- if (process.env.DISCORD_WEBHOOK) {
- await fetch(process.env.DISCORD_WEBHOOK, {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
+ 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
},
- body: JSON.stringify({
- description: "Here's the latest Vencord Report!",
- username: "Vencord+ Reporter" + (CANARY ? " (Canary)" : ""),
- embeds: [
- {
- 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
- }
- ]
- })
+ 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({
+ username: "Vencord+ Reporter" + (CANARY ? " (Canary)" : ""),
+ embeds
+ });
+
+ const headers = {
+ "Content-Type": "application/json"
+ };
+
+ // functions similar to https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
+ // used by venbot to ensure webhook invocations are genuine (since we will pass the webhook url as a workflow input which is publicly visible)
+ // generate a secret with something like `openssl rand -hex 128`
+ if (process.env.WEBHOOK_SECRET) {
+ headers["X-Signature"] = "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET).update(body).digest("hex");
+ }
+
+ await fetch(process.env.WEBHOOK_URL, {
+ method: "POST",
+ headers,
+ body
}).then(res => {
- if (!res.ok) console.error(`Webhook failed with status ${res.status}`);
- else console.error("Posted to Discord Webhook successfully");
+ if (!res.ok) logStderr(`Webhook failed with status ${res.status}`);
+ else logStderr("Posted to Webhook successfully");
});
}
}
@@ -186,10 +224,13 @@ page.on("console", async e => {
const level = e.type();
const rawArgs = e.args();
- async function getText() {
+ async function getText(skipFirst = true) {
+ let args = e.args();
+ if (skipFirst) args = args.slice(1);
+
try {
return await Promise.all(
- e.args().map(async a => {
+ args.map(async a => {
return await maybeGetError(a) || await a.jsonValue();
})
).then(a => a.join(" ").trim());
@@ -202,6 +243,12 @@ page.on("console", async e => {
const isVencord = firstArg === "[Vencord+]";
const isDebug = firstArg === "[PUP_DEBUG]";
+ const isReporterMeta = firstArg === "[REPORTER_META]";
+
+ if (isReporterMeta) {
+ metaData = await rawArgs[1].jsonValue() as any;
+ return;
+ }
outer:
if (isVencord) {
@@ -215,18 +262,21 @@ page.on("console", async e => {
switch (tag) {
case "WebpackInterceptor:":
- const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
- if (!patchFailMatch) break;
+ const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/);
+ const patchSlowMatch = message.match(/Patch by (.+?) (took [\d.]+?ms) \(Module id is (.+?)\): (.+)/);
+ const match = patchFailMatch ?? patchSlowMatch;
+ if (!match) break;
- console.error(await getText());
+ logStderr(await getText());
process.exitCode = 1;
- const [, plugin, type, id, regex] = patchFailMatch;
- report.badPatches.push({
+ const [, plugin, type, id, regex] = match;
+ const list = patchFailMatch ? report.badPatches : report.slowPatches;
+ list.push({
plugin,
type,
id,
- match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
+ match: regex,
error: await maybeGetError(e.args()[3])
});
@@ -235,7 +285,7 @@ page.on("console", async e => {
const failedToStartMatch = message.match(/Failed to start (.+)/);
if (!failedToStartMatch) break;
- console.error(await getText());
+ logStderr(await getText());
process.exitCode = 1;
const [, name] = failedToStartMatch;
@@ -246,7 +296,7 @@ page.on("console", async e => {
break;
case "LazyChunkLoader:":
- console.error(await getText());
+ logStderr(await getText());
switch (message) {
case "A fatal error occurred:":
@@ -255,7 +305,7 @@ page.on("console", async e => {
break;
case "Reporter:":
- console.error(await getText());
+ logStderr(await getText());
switch (message) {
case "A fatal error occurred:":
@@ -273,47 +323,36 @@ page.on("console", async e => {
}
if (isDebug) {
- console.error(await getText());
+ logStderr(await getText());
} else if (level === "error") {
- const text = await getText();
+ const text = await getText(false);
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
report.ignoredErrors.push(text);
} else {
- console.error("[Unexpected Error]", text);
+ logStderr("[Unexpected Error]", text);
report.otherErrors.push(text);
}
}
}
});
-page.on("error", e => console.error("[Error]", e.message));
+page.on("error", e => logStderr("[Error]", e.message));
page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return;
- if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
- console.error("[Page Error]", e.message);
+ if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module") && !/^.{1,2}$/.test(e.message)) {
+ logStderr("[Page Error]", e.message);
report.otherErrors.push(e.message);
} else {
report.ignoredErrors.push(e.message);
}
});
-async function reporterRuntime(token: string) {
- Vencord.Webpack.waitFor(
- "loginToken",
- m => {
- console.log("[PUP_DEBUG]", "Logging in with token...");
- m.loginToken(token);
- }
- );
-}
-
await page.evaluateOnNewDocument(`
if (location.host.endsWith("discord.com")) {
${readFileSync("./dist/browser.js", "utf-8")};
- (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
}
`);
diff --git a/src/Vencord.ts b/src/Vencord.ts
index 1a6b5404..03159e07 100644
--- a/src/Vencord.ts
+++ b/src/Vencord.ts
@@ -23,6 +23,7 @@ export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater";
export * as Webpack from "./webpack";
+export * as WebpackPatcher from "./webpack/patchWebpack";
export { PlainSettings, Settings };
import "./utils/quickCss";
diff --git a/src/api/Settings.ts b/src/api/Settings.ts
index 171df99c..50af85d3 100644
--- a/src/api/Settings.ts
+++ b/src/api/Settings.ts
@@ -32,9 +32,10 @@ export interface Settings {
autoUpdate: boolean;
autoUpdateNotification: boolean,
useQuickCss: boolean;
+ eagerPatches: boolean;
+ enabledThemes: string[];
enableReactDevtools: boolean;
themeLinks: string[];
- enabledThemes: string[];
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
@@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
autoUpdateNotification: true,
useQuickCss: false,
themeLinks: [],
+ eagerPatches: IS_REPORTER,
enabledThemes: [],
enableReactDevtools: false,
frameless: false,
diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx
index f3a8e1dd..f930a40d 100644
--- a/src/components/VencordSettings/PatchHelperTab.tsx
+++ b/src/components/VencordSettings/PatchHelperTab.tsx
@@ -65,7 +65,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
}
const canonicalMatch = canonicalizeMatch(new RegExp(match));
try {
- const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
+ const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins["YourPlugin"]');
var patched = src.replace(canonicalMatch, canonicalReplace as string);
setReplacementError(void 0);
} catch (e) {
diff --git a/src/debug/Tracer.ts b/src/debug/Tracer.ts
index 7d80f425..37ea4cc0 100644
--- a/src/debug/Tracer.ts
+++ b/src/debug/Tracer.ts
@@ -23,35 +23,61 @@ if (IS_DEV || IS_REPORTER) {
var logger = new Logger("Tracer", "#FFD166");
}
-const noop = function () { };
-
-export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
+export const beginTrace = !(IS_DEV || IS_REPORTER) ? () => { } :
function beginTrace(name: string, ...args: any[]) {
- if (name in traces)
+ if (name in traces) {
throw new Error(`Trace ${name} already exists!`);
+ }
traces[name] = [performance.now(), args];
};
-export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
- const end = performance.now();
+export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
+ function finishTrace(name: string) {
+ const end = performance.now();
- const [start, args] = traces[name];
- delete traces[name];
+ const [start, args] = traces[name];
+ delete traces[name];
- logger.debug(`${name} took ${end - start}ms`, args);
-};
+ const totalTime = end - start;
+ logger.debug(`${name} took ${totalTime}ms`, args);
+
+ return totalTime;
+ };
type Func = (...args: any[]) => any;
type TraceNameMapper = (...args: Parameters) => string;
-const noopTracer =
- (name: string, f: F, mapper?: TraceNameMapper) => f;
+function noopTracerWithResults(name: string, f: F, mapper?: TraceNameMapper) {
+ return function (this: unknown, ...args: Parameters): [ReturnType, number] {
+ return [f.apply(this, args), 0];
+ };
+}
+
+function noopTracer(name: string, f: F, mapper?: TraceNameMapper) {
+ return f;
+}
+
+export const traceFunctionWithResults = !(IS_DEV || IS_REPORTER)
+ ? noopTracerWithResults
+ : function traceFunctionWithResults(name: string, f: F, mapper?: TraceNameMapper): (this: unknown, ...args: Parameters) => [ReturnType, number] {
+ return function (this: unknown, ...args: Parameters) {
+ const traceName = mapper?.(...args) ?? name;
+
+ beginTrace(traceName, ...arguments);
+ try {
+ return [f.apply(this, args), finishTrace(traceName)];
+ } catch (e) {
+ finishTrace(traceName);
+ throw e;
+ }
+ };
+ };
export const traceFunction = !(IS_DEV || IS_REPORTER)
? noopTracer
: function traceFunction(name: string, f: F, mapper?: TraceNameMapper): F {
- return function (this: any, ...args: Parameters) {
+ return function (this: unknown, ...args: Parameters) {
const traceName = mapper?.(...args) ?? name;
beginTrace(traceName, ...arguments);
diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts
index c7f8047d..9c06e3aa 100644
--- a/src/debug/loadLazyChunks.ts
+++ b/src/debug/loadLazyChunks.ts
@@ -8,23 +8,26 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack";
import { wreq } from "@webpack";
-
-const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
+import { AnyModuleFactory, ModuleFactory } from "@webpack/wreq.d";
export async function loadLazyChunks() {
+ const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
+
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
- const validChunks = new Set();
- const invalidChunks = new Set();
- const deferredRequires = new Set();
+ const validChunks = new Set();
+ const invalidChunks = new Set();
+ const deferredRequires = new Set();
- let chunksSearchingResolve: (value: void | PromiseLike) => void;
- const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r);
+ const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers();
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;
+ /* This regex loads all language packs which makes webpack finds testing extremely slow, so for now, lets use one which doesnt include those
+ const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
+ */
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
let foundCssDebuggingLoad = false;
@@ -34,12 +37,15 @@ export async function loadLazyChunks() {
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
- const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
+ const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
const shouldForceDefer = false;
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
- const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
+ const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => {
+ const numChunkId = Number(m[1]);
+ return Number.isNaN(numChunkId) ? m[1] : numChunkId;
+ }) : [];
if (chunkIds.length === 0) {
return;
@@ -74,7 +80,8 @@ export async function loadLazyChunks() {
}
if (!invalidChunkGroup) {
- validChunkGroups.add([chunkIds, Number(entryPoint)]);
+ const numEntryPoint = Number(entryPoint);
+ validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
}
}));
@@ -82,7 +89,7 @@ export async function loadLazyChunks() {
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
- Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
+ Promise.all(chunkIds.map(id => wreq.e(id)))
)
);
@@ -94,7 +101,7 @@ export async function loadLazyChunks() {
continue;
}
- if (wreq.m[entryPoint]) wreq(entryPoint as any);
+ if (wreq.m[entryPoint]) wreq(entryPoint);
} catch (err) {
console.error(err);
}
@@ -122,41 +129,44 @@ export async function loadLazyChunks() {
}, 0);
}
- Webpack.factoryListeners.add(factory => {
+ function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
let isResolved = false;
- searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
+ searchAndLoadLazyChunks(String(factory))
+ .then(() => isResolved = true)
+ .catch(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
- });
+ }
- for (const factoryId in wreq.m) {
- let isResolved = false;
- searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
-
- chunksSearchPromises.push(() => isResolved);
+ Webpack.factoryListeners.add(factoryListener);
+ for (const moduleId in wreq.m) {
+ factoryListener(wreq.m[moduleId]);
}
await chunksSearchingDone;
+ Webpack.factoryListeners.delete(factoryListener);
// Require deferred entry points
for (const deferredRequire of deferredRequires) {
- wreq!(deferredRequire as any);
+ wreq(deferredRequire);
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
- const allChunks = [] as number[];
+ const allChunks = [] as PropertyKey[];
// Matches "id" or id:
- for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
+ for (const currentMatch of String(wreq.u).matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
- allChunks.push(Number(id));
+ const numId = Number(id);
+ allChunks.push(Number.isNaN(numId) ? id : numId);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
- // Chunks that are not loaded (not used) by Discord code anymore
+ // Chunks which our regex could not catch to load
+ // It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
@@ -166,12 +176,9 @@ export async function loadLazyChunks() {
.then(r => r.text())
.then(t => t.includes("importScripts("));
- // Loads and requires a chunk
+ // Loads the chunk. Currently this only happens with the language packs which are loaded differently
if (!isWorkerAsset) {
- await wreq.e(id as any);
- // Technically, the id of the chunk does not match the entry point
- // But, still try it because we have no way to get the actual entry point
- if (wreq.m[id]) wreq(id as any);
+ await wreq.e(id);
}
}));
diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts
index aef6ba3c..2ca83b7f 100644
--- a/src/debug/runReporter.ts
+++ b/src/debug/runReporter.ts
@@ -6,28 +6,56 @@
import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack";
-import { patches } from "plugins";
+import { getBuildNumber, patchTimings } from "@webpack/patcher";
+import { addPatch, patches } from "../plugins";
import { loadLazyChunks } from "./loadLazyChunks";
-const ReporterLogger = new Logger("Reporter");
-
async function runReporter() {
+ const ReporterLogger = new Logger("Reporter");
+
try {
ReporterLogger.log("Starting test...");
- let loadLazyChunksResolve: (value: void | PromiseLike) => void;
- const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r);
+ const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers();
+
+ // The main patch for starting the reporter chunk loading
+ addPatch({
+ find: '"Could not find app-mount"',
+ replacement: {
+ match: /(?<="use strict";)/,
+ replace: "Vencord.Webpack._initReporter();"
+ }
+ }, "Vencord Reporter");
+
+ // @ts-ignore
+ Vencord.Webpack._initReporter = function () {
+ // initReporter is called in the patched entry point of Discord
+ // setImmediate to only start searching for lazy chunks after Discord initialized the app
+ setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
+ };
- Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
await loadLazyChunksDone;
+ if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
+ console.log("[REPORTER_META]", {
+ buildNumber: getBuildNumber(),
+ buildHash: window.GLOBAL_ENV.SENTRY_TAGS.buildId
+ });
+ }
+
for (const patch of patches) {
if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}
+ for (const [plugin, moduleId, match, totalTime] of patchTimings) {
+ if (totalTime > 5) {
+ new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
+ }
+ }
+
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
let method = searchType;
@@ -50,9 +78,9 @@ async function runReporter() {
result = await Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null;
} 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");
} else {
// @ts-ignore
@@ -88,4 +116,6 @@ async function runReporter() {
}
}
-runReporter();
+// Run after the Vencord object has been created.
+// We need to add extra properties to it, and it is only created after all of Vencord code has ran
+setTimeout(runReporter, 0);
diff --git a/src/globals.d.ts b/src/globals.d.ts
index e20ca4b7..4456564c 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -64,13 +64,8 @@ declare global {
export var Vesktop: any;
export var VesktopNative: any;
- interface Window {
- webpackChunkdiscord_app: {
- push(chunk: any): any;
- pop(): any;
- };
+ interface Window extends Record {
_: LoDashStatic;
- [k: string]: any;
}
}
diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts
index d552037f..30920a06 100644
--- a/src/plugins/_core/noTrack.ts
+++ b/src/plugins/_core/noTrack.ts
@@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types";
+import { WebpackRequire } from "@webpack/wreq.d";
const settings = definePluginSettings({
disableAnalytics: {
@@ -81,9 +82,9 @@ export default definePlugin({
Object.defineProperty(Function.prototype, "g", {
configurable: true,
- set(v: any) {
+ set(this: WebpackRequire, globalObj: WebpackRequire["g"]) {
Object.defineProperty(this, "g", {
- value: v,
+ value: globalObj,
configurable: true,
enumerable: true,
writable: true
@@ -92,11 +93,11 @@ export default definePlugin({
// Ensure this is most likely the Sentry WebpackInstance.
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: ) to include it
const { stack } = new Error();
- if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
+ if (this.c != null || !stack?.includes("http") || !String(this).includes("exports:{}")) {
return;
}
- const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
+ const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];
if (!assetPath) {
return;
}
@@ -106,7 +107,8 @@ export default definePlugin({
srcRequest.send();
// Final condition to see if this is the Sentry WebpackInstance
- if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
+ // This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies
+ if (!srcRequest.responseText.includes(".DiscordSentry=")) {
return;
}
diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx
index 8987d8c5..8eb6a6db 100644
--- a/src/plugins/_core/settings.tsx
+++ b/src/plugins/_core/settings.tsx
@@ -158,6 +158,9 @@ export default definePlugin({
aboveActivity: getIntlMessage("ACTIVITY_SETTINGS")
};
+ if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS"))
+ return firstChild === "PREMIUM";
+
return header === names[settingsLocation];
} catch {
return firstChild === "PREMIUM";
diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts
index 5d5709f4..c0a4cb92 100644
--- a/src/plugins/consoleShortcuts/index.ts
+++ b/src/plugins/consoleShortcuts/index.ts
@@ -82,6 +82,8 @@ function makeShortcuts() {
wp: Webpack,
wpc: { getter: () => Webpack.cache },
wreq: { getter: () => Webpack.wreq },
+ wpPatcher: { getter: () => Vencord.WebpackPatcher },
+ wpInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances },
wpsearch: search,
wpex: extract,
wpexs: (code: string) => extract(findModuleId(code)!),
diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx
index a495907b..4ac2e993 100644
--- a/src/plugins/devCompanion.dev/index.tsx
+++ b/src/plugins/devCompanion.dev/index.tsx
@@ -160,7 +160,7 @@ function initWs(isManual = false) {
return reply("Expected exactly one 'find' matches, found " + keys.length);
const mod = candidates[keys[0]];
- let src = String(mod.original ?? mod).replaceAll("\n", "");
+ let src = String(mod).replaceAll("\n", "");
if (src.startsWith("function(")) {
src = "0," + src;
@@ -173,7 +173,7 @@ function initWs(isManual = false) {
try {
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);
diff --git a/src/plugins/friendInvites/index.ts b/src/plugins/friendInvites/index.ts
index 3c5a324f..0af611bc 100644
--- a/src/plugins/friendInvites/index.ts
+++ b/src/plugins/friendInvites/index.ts
@@ -31,7 +31,7 @@ export default definePlugin({
{
name: "create friend invite",
description: "Generates a friend invite link.",
- inputType: ApplicationCommandInputType.BOT,
+ inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (args, ctx) => {
const invite = await FriendInvites.createFriendInvite();
@@ -48,7 +48,7 @@ export default definePlugin({
{
name: "view friend invites",
description: "View a list of all generated friend invites.",
- inputType: ApplicationCommandInputType.BOT,
+ inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (_, ctx) => {
const invites = await FriendInvites.getAllFriendInvites();
const friendInviteList = invites.map(i =>
@@ -67,7 +67,7 @@ export default definePlugin({
{
name: "revoke friend invites",
description: "Revokes all generated friend invites.",
- inputType: ApplicationCommandInputType.BOT,
+ inputType: ApplicationCommandInputType.BUILT_IN,
execute: async (_, ctx) => {
await FriendInvites.revokeFriendInvites();
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index 306ac737..4a268868 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -28,9 +28,10 @@ import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/Messag
import { Settings, SettingsStore } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { Logger } from "@utils/Logger";
-import { canonicalizeFind } from "@utils/patches";
+import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
+import { patches } from "@webpack/patcher";
import { FluxEvents } from "@webpack/types";
import Plugins from "~plugins";
@@ -41,7 +42,7 @@ const logger = new Logger("PluginManager", "#a6d189");
export const PMLogger = logger;
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 */
let enabledPluginsSubscribedFlux = false;
@@ -58,7 +59,7 @@ export function isPluginEnabled(p: string) {
) ?? false;
}
-export function addPatch(newPatch: Omit, pluginName: string) {
+export function addPatch(newPatch: Omit, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) {
const patch = newPatch as Patch;
patch.plugin = pluginName;
@@ -74,10 +75,12 @@ export function addPatch(newPatch: Omit, pluginName: string) {
patch.replacement = [patch.replacement];
}
- if (IS_REPORTER) {
- patch.replacement.forEach(r => {
- delete r.predicate;
- });
+ for (const replacement of patch.replacement) {
+ canonicalizeReplacement(replacement, pluginPath);
+
+ if (IS_REPORTER) {
+ delete replacement.predicate;
+ }
}
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());
diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts
index 66966fc0..e6c5dc28 100644
--- a/src/utils/Logger.ts
+++ b/src/utils/Logger.ts
@@ -32,7 +32,7 @@ export class Logger {
constructor(public name: string, public color: string = "white") { }
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
- if (IS_REPORTER && IS_WEB) {
+ if (IS_REPORTER && IS_WEB && !IS_VESKTOP) {
console[level]("[Vencord+]", this.name + ":", ...args);
return;
}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 230d96cd..4df51c1e 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
export const VENBOT_USER_ID = "1017176847865352332";
export const VENCORD_GUILD_ID = "1015060230222131221";
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
index 797a2157..bb7b02f2 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -104,6 +104,11 @@ export function pluralise(amount: number, singular: string, plural = singular +
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
}
+export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) {
+ if (args.some(arg => arg == null)) return "";
+ return String.raw({ raw: strings }, ...args);
+}
+
export function tryOrElse(func: () => T, fallback: T): T {
try {
const res = func();
diff --git a/src/utils/patches.ts b/src/utils/patches.ts
index 097c6456..b212e624 100644
--- a/src/utils/patches.ts
+++ b/src/utils/patches.ts
@@ -41,16 +41,17 @@ export function canonicalizeMatch(match: T): T {
}
const canonSource = partialCanon.replaceAll("\\i", String.raw`(?:[A-Za-z_$][\w$]*)`);
- return new RegExp(canonSource, match.flags) as T;
+ const canonRegex = new RegExp(canonSource, match.flags);
+ canonRegex.toString = match.toString.bind(match);
+
+ return canonRegex as T;
}
-export function canonicalizeReplace(replace: T, pluginName: string): T {
- const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
-
+export function canonicalizeReplace(replace: T, pluginPath: string): T {
if (typeof replace !== "function")
- return replace.replaceAll("$self", self) as T;
+ return replace.replaceAll("$self", pluginPath) as T;
- return ((...args) => replace(...args).replaceAll("$self", self)) as T;
+ return ((...args) => replace(...args).replaceAll("$self", pluginPath)) as T;
}
export function canonicalizeDescriptor(descriptor: TypedPropertyDescriptor, canonicalize: (value: T) => T) {
@@ -65,12 +66,12 @@ export function canonicalizeDescriptor(descriptor: TypedPropertyDescriptor
return descriptor;
}
-export function canonicalizeReplacement(replacement: Pick, plugin: string) {
+export function canonicalizeReplacement(replacement: Pick, pluginPath: string) {
const descriptors = Object.getOwnPropertyDescriptors(replacement);
descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch);
descriptors.replace = canonicalizeDescriptor(
descriptors.replace,
- replace => canonicalizeReplace(replace, plugin),
+ replace => canonicalizeReplace(replace, pluginPath),
);
Object.defineProperties(replacement, descriptors);
}
diff --git a/src/utils/types.ts b/src/utils/types.ts
index e226c5ba..4ff30b78 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -43,6 +43,10 @@ export interface PatchReplacement {
replace: string | ReplaceFn;
/** A function which returns whether this patch replacement should be applied */
predicate?(): boolean;
+ /** The minimum build number for this patch to be applied */
+ fromBuild?: number;
+ /** The maximum build number for this patch to be applied */
+ toBuild?: number;
}
export interface Patch {
@@ -59,6 +63,10 @@ export interface Patch {
group?: boolean;
/** A function which returns whether this patch should be applied */
predicate?(): boolean;
+ /** The minimum build number for this patch to be applied */
+ fromBuild?: number;
+ /** The maximum build number for this patch to be applied */
+ toBuild?: number;
}
export interface PluginAuthor {
diff --git a/src/webpack/common/menu.ts b/src/webpack/common/menu.ts
index c45b6996..9896b3a2 100644
--- a/src/webpack/common/menu.ts
+++ b/src/webpack/common/menu.ts
@@ -25,7 +25,7 @@ export const Menu = {} as t.Menu;
// Relies on .name properties added by the MenuItemDemanglerAPI
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
// we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module
- const module = wreq(id as any);
+ const module = wreq(id);
for (const e of Object.values(module)) {
if (typeof e === "function" && e.name.startsWith("Menu")) {
diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts
index 4eb075a1..e968f9c0 100644
--- a/src/webpack/common/utils.ts
+++ b/src/webpack/common/utils.ts
@@ -61,9 +61,9 @@ export const useDrag = findByCodeLazy("useDrag::spec.begin was deprecated");
export const useDrop = findByCodeLazy(".options);return", ".collect,");
export const i18n = mapMangledModuleLazy('defaultLocale:"en-US"', {
+ t: filters.byProps(runtimeHashMessageKey("DISCORD")),
intl: filters.byProps("string", "format"),
- t: filters.byProps(runtimeHashMessageKey("DISCORD"))
-});
+}, true);
export let SnowflakeUtils: t.SnowflakeUtils;
waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m);
diff --git a/src/webpack/index.ts b/src/webpack/index.ts
index 036c2a3f..6f1fd25b 100644
--- a/src/webpack/index.ts
+++ b/src/webpack/index.ts
@@ -18,3 +18,4 @@
export * as Common from "./common";
export * from "./webpack";
+export * from "./wreq.d";
diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts
index 1122d55b..408d3b70 100644
--- a/src/webpack/patchWebpack.ts
+++ b/src/webpack/patchWebpack.ts
@@ -1,353 +1,641 @@
/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
-*/
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated, Nuckyz, and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
-import { WEBPACK_CHUNK } from "@utils/constants";
+import { Settings } from "@api/Settings";
+import { makeLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
+import { interpolateIfDefined } from "@utils/misc";
import { canonicalizeReplacement } from "@utils/patches";
-import { PatchReplacement } from "@utils/types";
-import { WebpackInstance } from "discord-types/other";
+import { Patch, PatchReplacement } from "@utils/types";
-import { traceFunction } from "../debug/Tracer";
-import { patches } from "../plugins";
-import { _initWebpack, _shouldIgnoreModule, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from ".";
+import { traceFunctionWithResults } from "../debug/Tracer";
+import { _blacklistBadModules, _initWebpack, factoryListeners, findModuleId, moduleListeners, waitForSubscriptions, wreq } from "./webpack";
+import { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, ModuleExports, 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_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource");
+export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy");
+export const allWebpackInstances = new Set();
+
+export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: PatchReplacement["match"], totalTime: number]>;
+
+export const getBuildNumber = makeLazy(() => {
+ try {
+ try {
+ if (wreq.m[128014]?.toString().includes("Trying to open a changelog for an invalid build number")) {
+ 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");
+ if (moduleId == null) {
+ return -1;
+ }
+
+ const exports = Object.values(wreq(moduleId));
+ if (exports.length !== 1 || typeof exports[0] !== "function") {
+ return -1;
+ }
+
+ const buildNumber = exports[0]();
+ return typeof buildNumber === "number" ? buildNumber : -1;
+ } catch {
+ return -1;
+ }
+});
+
+export function getFactoryPatchedSource(moduleId: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
+ 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");
-let webpackChunk: any[];
+/** Whether we tried to fallback to the WebpackRequire of the factory, or disabled patches */
+let wreqFallbackApplied = false;
-// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed
-// This way we can patch the factory of everything being pushed to the modules array
-Object.defineProperty(window, WEBPACK_CHUNK, {
- configurable: true,
-
- get: () => webpackChunk,
- set: v => {
- if (v?.push) {
- if (!v.push.$$vencordOriginal) {
- logger.info(`Patching ${WEBPACK_CHUNK}.push`);
- patchPush(v);
-
- // @ts-ignore
- delete window[WEBPACK_CHUNK];
- window[WEBPACK_CHUNK] = v;
- }
- }
-
- webpackChunk = v;
+const define: typeof Reflect.defineProperty = (target, p, attributes) => {
+ if (Object.hasOwn(attributes, "value")) {
+ attributes.writable = true;
}
-});
-// wreq.m is the webpack module factory.
-// normally, this is populated via webpackGlobal.push, which we patch below.
-// However, Discord has their .m prepopulated.
-// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
-Object.defineProperty(Function.prototype, "m", {
- configurable: true,
+ return Reflect.defineProperty(target, p, {
+ configurable: true,
+ enumerable: true,
+ ...attributes
+ });
+};
- set(v: any) {
- Object.defineProperty(this, "m", {
- value: v,
- configurable: true,
- enumerable: true,
- writable: true
- });
+// wreq.m is the Webpack object containing module factories. It is pre-populated with factories, and is also populated via webpackGlobal.push
+// We use this setter to intercept when wreq.m is defined and setup our setters which decide whether we should patch these module factories
+// and the Webpack instance where they are being defined.
- // When using react devtools or other extensions, we may also catch their webpack here.
- // This ensures we actually got the right one
+// Factories can be patched in two ways. Eagerly or lazily.
+// 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.
+
+// Factories are always wrapped in a proxy, which allows us to intercept the call to them, patch if they werent eagerly patched,
+// and call them with our wrapper which notifies our listeners.
+
+// 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.
+
+// If this is the main Webpack, we also set up the internal references to WebpackRequire.
+define(Function.prototype, "m", {
+ enumerable: false,
+
+ set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) {
+ define(this, "m", { value: originalModules });
+
+ // Ensure this is likely one of Discord main Webpack instances.
+ // We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here.
const { stack } = new Error();
- if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(v)) {
+ if (!stack?.includes("http") || stack.match(/at \d+? \(/) || !String(this).includes("exports:{}")) {
return;
}
- const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "";
- logger.info("Found Webpack module factory", fileName);
+ const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
- patchFactories(v);
+ // 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.
+ // So if the setter is called with the Discord bundlePath, this means we should patch this instance and initialize the internal references to WebpackRequire.
+ define(this, "p", {
+ enumerable: false,
- // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property.
- // So if the setter is called, this means we can initialize the internal references to WebpackRequire.
- Object.defineProperty(this, "p", {
- configurable: true,
+ set(this: AnyWebpackRequire, bundlePath: NonNullable) {
+ define(this, "p", { value: bundlePath });
+ clearTimeout(bundlePathTimeout);
- set(this: WebpackInstance, bundlePath: string) {
- Object.defineProperty(this, "p", {
- value: bundlePath,
- configurable: true,
- enumerable: true,
- writable: true
- });
+ if (bundlePath !== "/assets/") {
+ return;
+ }
- clearTimeout(setterTimeout);
- if (bundlePath !== "/assets/") return;
+ patchThisInstance();
- logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`);
- _initWebpack(this);
-
- for (const beforeInitListener of beforeInitListeners) {
- beforeInitListener(this);
+ if (wreq == null && this.c != null) {
+ logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire");
+ _initWebpack(this as WebpackRequire);
}
}
});
- // setImmediate to clear this property setter if this is not the main Webpack.
- // If this is the main Webpack, wreq.p will always be set before the timeout runs.
- const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
+
+ // In the past, the sentry Webpack instance which we also wanted to patch used to rely on chunks being loaded before initting sentry.
+ // 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.
+ // 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.
+ // 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) {
+ define(this, "O", { value: onChunksLoaded });
+ clearTimeout(onChunksLoadedTimeout);
+
+ const wreq = this;
+ define(onChunksLoaded, "j", {
+ enumerable: false,
+
+ set(this: NonNullable, j: NonNullable["j"]) {
+ define(this, "j", { value: j });
+
+ if (wreq.p == null) {
+ patchThisInstance();
+ }
+ }
+ });
+ }
+ });
+
+ // If neither of these properties setters were triggered, delete them as they are not needed anymore.
+ const bundlePathTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
+ const onChunksLoadedTimeout = setTimeout(() => Reflect.deleteProperty(this, "O"), 0);
+
+ /**
+ * 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: object, getters: object) {
+ for (const key in getters) {
+ if (Object.hasOwn(getters, key) && !Object.hasOwn(exports, key)) {
+ Object.defineProperty(exports, key, {
+ enumerable: true,
+ configurable: true,
+ get: getters[key],
+ });
+ }
+ }
+ };
+ };
}
});
-function patchPush(webpackGlobal: any) {
- function handlePush(chunk: any) {
- try {
- patchFactories(chunk[1]);
- } catch (err) {
- logger.error("Error in handlePush", err);
+// The proxy for patching eagerly and/or wrapping factories in their proxy.
+const moduleFactoriesHandler: ProxyHandler = {
+ /*
+ 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
+ and that requires defining additional traps for keeping the object working
+
+ // Proxies on the prototype don't intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined,
+ // to avoid Reflect.get having no effect and causing a stack overflow
+ get(target, p, receiver) {
+ return undefined;
+ },
+ // Same thing as get
+ has(target, p) {
+ return false;
+ },
+ */
+
+ set: updateExistingOrProxyFactory
+};
+
+// The proxy for patching lazily and/or running factories with our wrapper.
+const moduleFactoryHandler: ProxyHandler = {
+ apply(target, thisArg: unknown, argArray: Parameters) {
+ // SYM_ORIGINAL_FACTORY means the factory has already been patched
+ if (target[SYM_ORIGINAL_FACTORY] != null) {
+ return runFactoryWithWrap(target as PatchedModuleFactory, thisArg, argArray);
}
- return handlePush.$$vencordOriginal.call(webpackGlobal, chunk);
+ // 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;
+ }
+
+ const originalFactory: AnyModuleFactory = target[SYM_ORIGINAL_FACTORY] ?? target;
+
+ // 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;
}
- handlePush.$$vencordOriginal = webpackGlobal.push;
- handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal);
- // Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
- // it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
- // If we then repatched the new push, we would end up with recursive patching, which leads to our patches
- // being applied multiple times.
- // Thus, override bind to use the original push
- handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args);
+ notifyFactoryListeners(moduleId, newFactory);
- Object.defineProperty(webpackGlobal, "push", {
- configurable: true,
-
- get: () => handlePush,
- set(v) {
- handlePush.$$vencordOriginal = v;
- }
- });
+ const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, newFactory) : newFactory, moduleFactoryHandler);
+ return Reflect.set(moduleFactories, moduleId, proxiedFactory, receiver);
}
-let webpackNotInitializedLogged = false;
-
-function patchFactories(factories: Record void>) {
- for (const id in factories) {
- let mod = factories[id];
-
- const originalMod = mod;
- const patchedBy = new Set();
-
- const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) {
- if (wreq == null && IS_DEV) {
- if (!webpackNotInitializedLogged) {
- webpackNotInitializedLogged = true;
- logger.error("WebpackRequire was not initialized, running modules without patches instead.");
- }
-
- return void originalMod(module, exports, require);
- }
-
- try {
- mod(module, exports, require);
- } catch (err) {
- // Just rethrow discord errors
- if (mod === originalMod) throw err;
-
- logger.error("Error in patched module", err);
- return void originalMod(module, exports, require);
- }
-
- exports = module.exports;
-
- if (!exports) return;
-
- if (require.c) {
- const shouldIgnoreModule = _shouldIgnoreModule(exports);
-
- if (shouldIgnoreModule) {
- Object.defineProperty(require.c, id, {
- value: require.c[id],
- enumerable: false,
- configurable: true,
- writable: true
- });
-
- return;
- }
- }
-
- 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 subscriptions) {
- try {
- if (exports && filter(exports)) {
- subscriptions.delete(filter);
- callback(exports, id);
- }
-
- if (typeof exports !== "object") {
- continue;
- }
-
- for (const exportKey in exports) {
- if (exports[exportKey] && filter(exports[exportKey])) {
- subscriptions.delete(filter);
- callback(exports[exportKey], id);
- }
- }
- } catch (err) {
- logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback);
- }
- }
- } as any as { toString: () => string, original: any, (...args: any[]): void; $$vencordPatchedSource?: string; };
-
- factory.toString = originalMod.toString.bind(originalMod);
- factory.original = originalMod;
-
- for (const factoryListener of factoryListeners) {
- try {
- factoryListener(originalMod);
- } catch (err) {
- logger.error("Error in Webpack factory listener:\n", err, factoryListener);
- }
+/**
+ * Update a duplicated factory that exists in any of the Webpack instances we track with a new original factory.
+ *
+ * @param moduleFactories The module factories where this new original factory is being set
+ * @param moduleId The id of the module
+ * @param newFactory The new original factory
+ * @param receiver The receiver of the factory
+ * @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(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget) {
+ let existingFactory: AnyModuleFactory | undefined;
+ let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined;
+ for (const wreq of allWebpackInstances) {
+ if (ignoreExistingInTarget && wreq.m === moduleFactories) {
+ continue;
}
- // Discords Webpack chunks for some ungodly reason contain random
- // newlines. Cyn recommended this workaround and it seems to work fine,
- // however this could potentially break code, so if anything goes weird,
- // this is probably why.
- // Additionally, `[actual newline]` is one less char than "\n", so if Discord
- // ever targets newer browsers, the minifier could potentially use this trick and
- // cause issues.
- //
- // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
- let code: string = "0," + mod.toString().replaceAll("\n", "");
+ if (Object.hasOwn(wreq.m, moduleId)) {
+ existingFactory = wreq.m[moduleId];
+ moduleFactoriesWithFactory = wreq.m;
+ break;
+ }
+ }
- for (let i = 0; i < patches.length; i++) {
- const patch = patches[i];
+ if (existingFactory != null) {
+ // Sanity check to make sure these factories are equal
+ if (String(newFactory) !== String(existingFactory)) {
+ return false;
+ }
- const moduleMatches = typeof patch.find === "string"
- ? code.includes(patch.find)
- : patch.find.test(code);
+ // If existingFactory exists in any of the Webpack instances we track, it's either wrapped in our proxy, or it has already been required.
+ // 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.
+ if (moduleFactoriesWithFactory !== moduleFactories && existingFactory[SYM_IS_PROXIED_FACTORY]) {
+ Reflect.set(moduleFactories, moduleId, existingFactory, receiver);
+ }
+ // Else, if it is not wrapped in our proxy, set this new original factory in all the instances
+ else {
+ defineInWebpackInstances(moduleId, newFactory);
+ }
- if (!moduleMatches) continue;
+ // Update existingFactory with the new original, if it does have a current original factory
+ if (existingFactory[SYM_ORIGINAL_FACTORY] != null) {
+ existingFactory[SYM_ORIGINAL_FACTORY] = newFactory;
+ }
- patchedBy.add(patch.plugin);
+ // 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];
+ }
- const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
- const previousMod = mod;
- const previousCode = code;
+ return true;
+ }
- // We change all patch.replacement to array in plugins/index
- for (const replacement of patch.replacement as PatchReplacement[]) {
- const lastMod = mod;
- const lastCode = code;
+ return false;
+}
- canonicalizeReplacement(replacement, patch.plugin);
+/**
+ * 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 });
+ }
+}
- try {
- const newCode = executePatch(replacement.match, replacement.replace as string);
- if (newCode === code) {
- if (!patch.noWarn) {
- logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
- if (IS_DEV) {
- logger.debug("Function Source:\n", code);
- }
+/**
+ * Notify all factory listeners.
+ *
+ * @param moduleId The id of the module
+ * @param factory The original factory to notify for
+ */
+function notifyFactoryListeners(moduleId: PropertyKey, factory: AnyModuleFactory) {
+ for (const factoryListener of factoryListeners) {
+ try {
+ factoryListener(factory, moduleId);
+ } catch (err) {
+ logger.error("Error in Webpack factory listener:\n", err, factoryListener);
+ }
+ }
+}
+
+/**
+ * Run a (possibly) patched module factory with a wrapper which notifies our listeners.
+ *
+ * @param patchedFactory The (possibly) patched module factory
+ * @param thisArg The `value` of the call to the factory
+ * @param argArray The arguments of the call to the factory
+ */
+function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unknown, argArray: Parameters) {
+ const originalFactory = patchedFactory[SYM_ORIGINAL_FACTORY];
+
+ if (patchedFactory === originalFactory) {
+ // @ts-expect-error Clear up ORIGINAL_FACTORY if the factory did not have any patch applied
+ delete patchedFactory[SYM_ORIGINAL_FACTORY];
+ }
+
+ let [module, exports, require] = argArray;
+
+ // Restore the original factory in all the module factories objects, discarding our proxy and allowing it to be garbage collected
+ defineInWebpackInstances(module.id, originalFactory);
+
+ if (wreq == null) {
+ if (!wreqFallbackApplied) {
+ wreqFallbackApplied = true;
+
+ // Make sure the require argument is actually the WebpackRequire function
+ if (typeof require === "function" && require.m != null && require.c != null) {
+ const { stack } = new Error();
+ const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
+
+ logger.warn(
+ "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called wrapped module factory (" +
+ `id: ${String(module.id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
+ ")"
+ );
+
+ // Could technically be wrong, but it's better than nothing
+ _initWebpack(require as WebpackRequire);
+ } else if (IS_DEV) {
+ 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;
+ }
+
+ logger.error("Error in patched module factory:\n", err);
+ return originalFactory.apply(thisArg, argArray);
+ }
+
+ exports = module.exports;
+ if (exports == null) {
+ return factoryReturn;
+ }
+
+ if (typeof require === "function" && require.c) {
+ if (_blacklistBadModules(require.c, exports, module.id)) {
+ return factoryReturn;
+ }
+ }
+
+ 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.
+ *
+ * @param moduleId The id of the module
+ * @param originalFactory The original module factory
+ * @returns The patched module factory
+ */
+function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory): PatchedModuleFactory {
+ // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
+ let code: string = "0," + String(originalFactory);
+ let patchedSource = code;
+ let patchedFactory = originalFactory;
+
+ const patchedBy = new Set();
+
+ for (let i = 0; i < patches.length; i++) {
+ const patch = patches[i];
+
+ const moduleMatches = typeof patch.find === "string"
+ ? code.includes(patch.find)
+ : (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
+
+ if (!moduleMatches) {
+ continue;
+ }
+
+ // Eager patches cannot retrieve the build number because this code runs before the module for it is loaded
+ const buildNumber = Settings.eagerPatches ? -1 : getBuildNumber();
+ const shouldCheckBuildNumber = !Settings.eagerPatches && buildNumber !== -1;
+
+ if (
+ shouldCheckBuildNumber &&
+ (patch.fromBuild != null && buildNumber < patch.fromBuild) ||
+ (patch.toBuild != null && buildNumber > patch.toBuild)
+ ) {
+ continue;
+ }
+
+ const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {
+ if (typeof match !== "string" && match.global) {
+ match.lastIndex = 0;
+ }
+
+ return code.replace(match, replace);
+ });
+
+ const previousCode = code;
+ const previousFactory = originalFactory;
+ let markedAsPatched = false;
+
+ // We change all patch.replacement to array in plugins/index
+ for (const replacement of patch.replacement as PatchReplacement[]) {
+ if (
+ shouldCheckBuildNumber &&
+ (replacement.fromBuild != null && buildNumber < replacement.fromBuild) ||
+ (replacement.toBuild != null && buildNumber > replacement.toBuild)
+ ) {
+ continue;
+ }
+
+ // TODO: remove once Vesktop has been updated to use addPatch
+ if (patch.plugin === "Vesktop") {
+ canonicalizeReplacement(replacement, "VCDP");
+ }
+
+ const lastCode = code;
+ const lastFactory = originalFactory;
+
+ try {
+ const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
+
+ if (IS_REPORTER) {
+ patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);
+ }
+
+ if (newCode === code) {
+ if (!patch.noWarn) {
+ logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);
+ if (IS_DEV) {
+ logger.debug("Function Source:\n", code);
}
-
- if (patch.group) {
- logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
- mod = previousMod;
- code = previousCode;
- patchedBy.delete(patch.plugin);
- break;
- }
-
- continue;
}
- code = newCode;
- mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
- } catch (err) {
- logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
-
- if (IS_DEV) {
- const changeSize = code.length - lastCode.length;
- const match = lastCode.match(replacement.match)!;
-
- // Use 200 surrounding characters of context
- const start = Math.max(0, match.index! - 200);
- const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
- // (changeSize may be negative)
- const endPatched = end + changeSize;
-
- const context = lastCode.slice(start, end);
- const patchedContext = code.slice(start, endPatched);
-
- // inline require to avoid including it in !IS_DEV builds
- const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
- let fmt = "%c %s ";
- const elements = [] as string[];
- for (const d of diff) {
- const color = d.removed
- ? "red"
- : d.added
- ? "lime"
- : "grey";
- fmt += "%c%s";
- elements.push("color:" + color, d.value);
- }
-
- logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
- logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
- const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
- logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
- }
-
- patchedBy.delete(patch.plugin);
-
if (patch.group) {
- logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
- mod = previousMod;
+ logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
code = previousCode;
+ patchedFactory = previousFactory;
+
+ if (markedAsPatched) {
+ patchedBy.delete(patch.plugin);
+ }
+
break;
}
- mod = lastMod;
- code = lastCode;
+ continue;
}
- }
- if (!patch.all) patches.splice(i--, 1);
+ code = newCode;
+ patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(moduleId)}`;
+ patchedFactory = (0, eval)(patchedSource);
+
+ if (!patchedBy.has(patch.plugin)) {
+ patchedBy.add(patch.plugin);
+ markedAsPatched = true;
+ }
+ } catch (err) {
+ logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\n`, err);
+
+ if (IS_DEV) {
+ diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);
+ }
+
+ if (markedAsPatched) {
+ patchedBy.delete(patch.plugin);
+ }
+
+ if (patch.group) {
+ logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
+ code = previousCode;
+ patchedFactory = previousFactory;
+ break;
+ }
+
+ code = lastCode;
+ patchedFactory = lastFactory;
+ }
}
- if (IS_DEV) {
- if (mod !== originalMod) {
- factory.$$vencordPatchedSource = String(mod);
- } else if (wreq != null) {
- const existingFactory = wreq.m[id];
-
- if (existingFactory != null) {
- factory.$$vencordPatchedSource = existingFactory.$$vencordPatchedSource;
- }
- }
+ if (!patch.all) {
+ patches.splice(i--, 1);
}
}
+
+ 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) {
+ const changeSize = code.length - lastCode.length;
+
+ // Use 200 surrounding characters of context
+ const start = Math.max(0, match.index! - 200);
+ const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
+ // (changeSize may be negative)
+ const endPatched = end + changeSize;
+
+ const context = lastCode.slice(start, end);
+ const patchedContext = code.slice(start, endPatched);
+
+ // Inline require to avoid including it in !IS_DEV builds
+ const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
+ let fmt = "%c %s ";
+ const elements: string[] = [];
+ for (const d of diff) {
+ const color = d.removed
+ ? "red"
+ : d.added
+ ? "lime"
+ : "grey";
+ fmt += "%c%s";
+ elements.push("color:" + color, d.value);
+ }
+
+ logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
+ logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
+ const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
+ logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
}
diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts
index 7ed79108..30180a7e 100644
--- a/src/webpack/webpack.ts
+++ b/src/webpack/webpack.ts
@@ -20,9 +20,10 @@ import { makeLazy, proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/lazyReact";
import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
-import type { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer";
+import { Flux } from "./common";
+import { AnyModuleFactory, AnyWebpackRequire, ModuleExports, WebpackRequire } from "./wreq";
const logger = new Logger("Webpack");
@@ -33,8 +34,8 @@ export let _resolveReady: () => void;
*/
export const onceReady = new Promise(r => _resolveReady = r);
-export let wreq: WebpackInstance;
-export let cache: WebpackInstance["c"];
+export let wreq: WebpackRequire;
+export let cache: WebpackRequire["c"];
export type FilterFn = (mod: any) => boolean;
@@ -89,33 +90,60 @@ export const filters = {
}
};
-export type CallbackFn = (mod: any, id: string) => void;
+export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void;
+export type FactoryListernFn = (factory: AnyModuleFactory, moduleId: PropertyKey) => void;
-export const subscriptions = new Map();
+export const waitForSubscriptions = new Map();
export const moduleListeners = new Set();
-export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>();
-export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
+export const factoryListeners = new Set();
-export function _initWebpack(webpackRequire: WebpackInstance) {
+export function _initWebpack(webpackRequire: WebpackRequire) {
wreq = webpackRequire;
cache = webpackRequire.c;
+
+ Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, {
+ value: "ModuleCache",
+ configurable: true,
+ writable: true,
+ enumerable: false
+ });
}
// Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too
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 === window) 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;
return false;
}
-export function _shouldIgnoreModule(exports: any) {
- if (_shouldIgnoreValue(exports)) {
+function makePropertyNonEnumerable(target: Object, key: PropertyKey) {
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
+ if (descriptor == null) return;
+
+ Reflect.defineProperty(target, key, {
+ ...descriptor,
+ enumerable: false
+ });
+}
+
+export function _blacklistBadModules(requireCache: NonNullable, exports: ModuleExports, moduleId: PropertyKey) {
+ if (shouldIgnoreValue(exports)) {
+ makePropertyNonEnumerable(requireCache, moduleId);
return true;
}
@@ -123,14 +151,16 @@ export function _shouldIgnoreModule(exports: any) {
return false;
}
- let allNonEnumerable = true;
+ let hasOnlyBadProperties = true;
for (const exportKey in exports) {
- if (!_shouldIgnoreValue(exports[exportKey])) {
- allNonEnumerable = false;
+ if (shouldIgnoreValue(exports[exportKey])) {
+ makePropertyNonEnumerable(exports, exportKey);
+ } else {
+ hasOnlyBadProperties = false;
}
}
- return allNonEnumerable;
+ return hasOnlyBadProperties;
}
let devToolsOpen = false;
@@ -398,7 +428,10 @@ export function findByCodeLazy(...code: CodeFilter) {
* Find a store by its displayName
*/
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)
handleModuleNotFound("findStore", name);
return res;
@@ -466,12 +499,27 @@ export function findExportedComponentLazy(...props: Prop
});
}
+function getAllPropertyNames(object: Object, includeNonEnumerable: boolean) {
+ const names = new Set();
+
+ const getKeys = includeNonEnumerable ? Object.getOwnPropertyNames : Object.keys;
+ do {
+ getKeys(object).forEach(name => 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)
* 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
+ * @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
*
* @example mapMangledModule("headerIdIsManaged:", {
@@ -479,7 +527,7 @@ export function findExportedComponentLazy(...props: Prop
* closeModal: filters.byCode("key==")
* })
*/
-export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record): Record {
+export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record, includeBlacklistedExports = false): Record {
const exports = {} as Record;
const id = findModuleId(...Array.isArray(code) ? code : [code]);
@@ -487,8 +535,9 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
return exports;
const mod = wreq(id as any);
+ const keys = getAllPropertyNames(mod, includeBlacklistedExports);
outer:
- for (const key in mod) {
+ for (const key of keys) {
const member = mod[key];
for (const newName in mappers) {
// if the current mapper matches this module
@@ -502,24 +551,13 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
});
/**
- * {@link mapMangledModule}, lazy.
-
- * 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==")
- * })
+ * lazy mapMangledModule
+ * @see {@link mapMangledModule}
*/
-export function mapMangledModuleLazy(code: string | RegExp | CodeFilter, mappers: Record): Record {
- if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]);
+export function mapMangledModuleLazy(code: string | RegExp | CodeFilter, mappers: Record, includeBlacklistedExports = false): Record {
+ 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,"?([^)]+?)"?\)\)/;
@@ -531,7 +569,7 @@ export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
* @returns A promise that resolves with a boolean whether the chunks were loaded
*/
-export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = DefaultExtractAndLoadChunksRegex) {
+export async function extractAndLoadChunks(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) {
const module = findModuleFactory(...code);
if (!module) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory");
@@ -544,7 +582,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false;
}
- const match = module.toString().match(canonicalizeMatch(matcher));
+ const match = String(module).match(canonicalizeMatch(matcher));
if (!match) {
const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code");
logger.warn(err, "Code:", code, "Matcher:", matcher);
@@ -557,8 +595,9 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
}
const [, rawChunkIds, entryPointId] = match;
- if (Number.isNaN(Number(entryPointId))) {
- const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
+
+ if (entryPointId == null) {
+ const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array or the entry point id");
logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
@@ -568,12 +607,19 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false;
}
+ const numEntryPoint = Number(entryPointId);
+ const entryPoint = Number.isNaN(numEntryPoint) ? entryPointId : numEntryPoint;
+
if (rawChunkIds) {
- const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => Number(m[1]));
+ const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map(m => {
+ const numChunkId = Number(m[1]);
+ return Number.isNaN(numChunkId) ? m[1] : numChunkId;
+ });
+
await Promise.all(chunkIds.map(id => wreq.e(id)));
}
- if (wreq.m[entryPointId] == null) {
+ if (wreq.m[entryPoint] == null) {
const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load");
logger.warn(err, "Code:", code, "Matcher:", matcher);
@@ -584,7 +630,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false;
}
- wreq(Number(entryPointId));
+ wreq(entryPoint);
return true;
}
@@ -621,7 +667,7 @@ export function waitFor(filter: string | PropsFilter | FilterFn, callback: Callb
if (existing) return void callback(existing, id);
}
- subscriptions.set(filter, callback);
+ waitForSubscriptions.set(filter, callback);
}
/**
@@ -637,7 +683,7 @@ export function search(...code: CodeFilter) {
const factories = wreq.m;
for (const id in factories) {
- const factory = factories[id].original ?? factories[id];
+ const factory = factories[id];
if (stringMatches(factory.toString(), code))
results[id] = factory;
diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts
new file mode 100644
index 00000000..ff28732c
--- /dev/null
+++ b/src/webpack/wreq.d.ts
@@ -0,0 +1,241 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated, Nuckyz and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { SYM_ORIGINAL_FACTORY, SYM_PATCHED_BY, SYM_PATCHED_SOURCE } from "./patchWebpack";
+
+export type ModuleExports = any;
+
+export type Module = {
+ id: PropertyKey;
+ loaded: boolean;
+ exports: ModuleExports;
+};
+
+/** 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 WebpackQueues = unique symbol | "__webpack_queues__";
+export type WebpackExports = unique symbol | "__webpack_exports__";
+export type WebpackError = unique symbol | "__webpack_error__";
+
+export type AsyncModulePromise = Promise & {
+ [WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any;
+ [WebpackExports]: ModuleExports;
+ [WebpackError]?: any;
+};
+
+export type AsyncModuleBody = (
+ handleAsyncDependencies: (deps: AsyncModulePromise[]) =>
+ Promise<() => ModuleExports[]> | (() => ModuleExports[]),
+ asyncResult: (error?: any) => void
+) => Promise;
+
+export type EnsureChunkHandlers = {
+ /**
+ * Ensures the js file for this chunk is loaded, or starts to load if it's not.
+ * @param chunkId The chunk id
+ * @param promises The promises array to add the loading promise to
+ */
+ j: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise) => void;
+ /**
+ * Ensures the css file for this chunk is loaded, or starts to load if it's not.
+ * @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
+ */
+ css: (this: EnsureChunkHandlers, chunkId: PropertyKey, promises: Promise) => 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;
+};
+
+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 OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & {
+ /** Check if a chunk has been loaded */
+ j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean;
+};
+
+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) */
+ m: Record;
+ /** The module cache, where all modules which have been WebpackRequire'd are stored */
+ c: Record;
+ // /**
+ // * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this:
+ // * @example
+ // * const fromObject = { a: 1 };
+ // * Object.keys(fromObject).forEach(key => {
+ // * if (key !== "default" && !Object.hasOwn(toObject, key)) {
+ // * Object.defineProperty(toObject, key, {
+ // * get: () => fromObject[key],
+ // * enumerable: true
+ // * });
+ // * }
+ // * });
+ // * @returns fromObject
+ // */
+ // es: (this: WebpackRequire, fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord;
+ /**
+ * Creates an async module. A module that which has top level await, or requires an export from an async module.
+ *
+ * The body function must be an async function. "module.exports" will become an {@link AsyncModulePromise}.
+ *
+ * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies:
+ * @example
+ * const factory = (module, exports, wreq) => {
+ * wreq.a(module, async (handleAsyncDependencies, asyncResult) => {
+ * try {
+ * const asyncRequireA = wreq(...);
+ *
+ * const asyncDependencies = handleAsyncDependencies([asyncRequire]);
+ * const [requireAResult] = asyncDependencies.then != null ? (await asyncDependencies)() : asyncDependencies;
+ *
+ * // Use the required module
+ * console.log(requireAResult);
+ *
+ * // Mark this async module as resolved
+ * asyncResult();
+ * } catch(error) {
+ * // Mark this async module as rejected with an error
+ * asyncResult(error);
+ * }
+ * }, false); // false because our module does not have an await after dealing with the async requires
+ * }
+ */
+ a: (this: WebpackRequire, module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void;
+ /** getDefaultExport function for compatibility with non-harmony modules */
+ n: (this: WebpackRequire, exports: any) => () => ModuleExports;
+ /**
+ * Create a fake namespace object, useful for faking an __esModule with a default export.
+ *
+ * mode & 1: Value is a module id, require it
+ *
+ * mode & 2: Merge all properties of value into the namespace
+ *
+ * mode & 4: Return value when already namespace object
+ *
+ * mode & 16: Return value when it's Promise-like
+ *
+ * mode & (8|1): Behave like require
+ */
+ t: (this: WebpackRequire, value: any, mode: number) => any;
+ /**
+ * Define getter functions for harmony exports. For every prop in "definiton" (the module exports), set a getter in "exports" for the getter function in the "definition", like this:
+ * @example
+ * const exports = {};
+ * const definition = { exportName: () => someExportedValue };
+ * for (const key in definition) {
+ * if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) {
+ * Object.defineProperty(exports, key, {
+ * get: definition[key],
+ * enumerable: true
+ * });
+ * }
+ * }
+ * // exports is now { exportName: someExportedValue } (but each value is actually a getter)
+ */
+ d: (this: WebpackRequire, exports: AnyRecord, definiton: AnyRecord) => void;
+ /** The ensure chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */
+ f: EnsureChunkHandlers;
+ /**
+ * 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.
+ */
+ e: (this: WebpackRequire, chunkId: PropertyKey) => Promise;
+ /** 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 */
+ k: (this: WebpackRequire, chunkId: PropertyKey) => string;
+ /** Get the filename for the js part of a chunk */
+ u: (this: WebpackRequire, chunkId: PropertyKey) => string;
+ /** The global object, will likely always be the window */
+ g: typeof globalThis;
+ /** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js "module.exports" from being set */
+ hmd: (this: WebpackRequire, module: Module) => any;
+ /** Shorthand for Object.prototype.hasOwnProperty */
+ o: typeof Object.prototype.hasOwnProperty;
+ /**
+ * Function to load a script tag. "done" is called when the loading has finished or a timeout has occurred.
+ * "done" will be attached to existing scripts loading if src === url or data-webpack === `${uniqueName}:${key}`,
+ * so it will be called when that existing script finishes loading.
+ */
+ l: (this: WebpackRequire, url: string, done: ScriptLoadDone, key?: string | number, chunkId?: PropertyKey) => void;
+ /** Defines __esModule on the exports, marking ES Modules compatibility as true */
+ r: (this: WebpackRequire, exports: ModuleExports) => void;
+ /** Node.js module decorator. Decorates a module as a Node.js module */
+ nmd: (this: WebpackRequire, module: Module) => any;
+ /**
+ * 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 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.
+ *
+ * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed.
+ */
+ O: OnChunksLoaded;
+ /**
+ * 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
+ */
+ v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise;
+ /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */
+ p: string;
+ /** The runtime id of the current runtime */
+ j: string;
+ /** Document baseURI or WebWorker location.href */
+ b: string;
+
+ /* rspack only */
+
+ /** rspack version */
+ rv: (this: WebpackRequire) => string;
+ /** rspack unique id */
+ ruid: string;
+};
+
+// Utility section for Vencord
+
+export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial> & {
+ /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
+ m: Record;
+};
+
+/** exports can be anything, however initially it is always an empty object */
+export type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void) & {
+ [SYM_PATCHED_SOURCE]?: string;
+ [SYM_PATCHED_BY]?: Set;
+};
+
+export type PatchedModuleFactory = AnyModuleFactory & {
+ [SYM_ORIGINAL_FACTORY]: AnyModuleFactory;
+ [SYM_PATCHED_SOURCE]?: string;
+ [SYM_PATCHED_BY]?: Set;
+};
+
+export type MaybePatchedModuleFactory = PatchedModuleFactory | AnyModuleFactory;
diff --git a/tsconfig.json b/tsconfig.json
index db6d0918..d2a42bd5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -29,7 +29,9 @@
"@shared/*": ["./shared/*"],
"@webpack/types": ["./webpack/common/types"],
"@webpack/common": ["./webpack/common"],
- "@webpack": ["./webpack/webpack"]
+ "@webpack": ["./webpack/webpack"],
+ "@webpack/patcher": ["./webpack/patchWebpack"],
+ "@webpack/wreq.d": ["./webpack/wreq.d"],
},
"plugins": [