diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ba22b1230..b1cbc3028 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -42,7 +42,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/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml
index a669c1a27..f1e53e4d0 100644
--- a/.github/workflows/reportBrokenPlugins.yml
+++ b/.github/workflows/reportBrokenPlugins.yml
@@ -1,9 +1,22 @@
name: Test Patches
on:
workflow_dispatch:
- schedule:
- # Every day at midnight
- - cron: 0 0 * * *
+ inputs:
+ discord_branch:
+ type: choice
+ description: "Discord Branch to test patches on"
+ options:
+ - both
+ - stable
+ - canary
+ default: both
+ webhook_url:
+ type: string
+ description: "Webhook URL that the report will be posted to. This will be visible for everyone, so DO NOT pass sensitive webhooks like discord webhook. This is meant to be used by Venbot."
+ required: false
+ # schedule:
+ # # Every day at midnight
+ # - cron: 0 0 * * *
jobs:
TestPlugins:
@@ -40,28 +53,43 @@ jobs:
- name: Build Vencord Reporter Version
run: pnpm buildReporter
- - name: Create Report
+ - name: Run Reporter
timeout-minutes: 10
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
esbuild scripts/generateReport.ts > dist/report.mjs
- node dist/report.mjs >> $GITHUB_STEP_SUMMARY
- env:
- DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
- DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- - name: Create Report (Canary)
- timeout-minutes: 10
- if: success() || failure() # even run if previous one failed
- run: |
- export PATH="$PWD/node_modules/.bin:$PATH"
- export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
- export USE_CANARY=true
+ stable_output_file=$(mktemp)
+ canary_output_file=$(mktemp)
- esbuild scripts/generateReport.ts > dist/report.mjs
- node dist/report.mjs >> $GITHUB_STEP_SUMMARY
+ pids=""
+
+ branch="${{ inputs.discord_branch }}"
+ if [[ "${{ github.event_name }}" = "schedule" ]]; then
+ branch="both"
+ fi
+
+ if [[ "$branch" = "both" || "$branch" = "stable" ]]; then
+ node dist/report.mjs > "$stable_output_file" &
+ pids+=" $!"
+ fi
+
+ if [[ "$branch" = "both" || "$branch" = "canary" ]]; then
+ USE_CANARY=true node dist/report.mjs > "$canary_output_file" &
+ pids+=" $!"
+ fi
+
+ exit_code=0
+ for pid in $pids; do
+ if ! wait "$pid"; then
+ exit_code=1
+ fi
+ done
+
+ cat "$stable_output_file" "$canary_output_file" >> $GITHUB_STEP_SUMMARY
+ exit $exit_code
env:
- DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
- DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ WEBHOOK_URL: ${{ inputs.webhook_url || secrets.DISCORD_WEBHOOK }}
+ WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}
diff --git a/.gitignore b/.gitignore
index 343842de8..884988311 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 357312b09..3463e46cf 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 ead061d65..dc243df7d 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 a55b0e547..12523d455 100644
--- a/browser/monacoWin.html
+++ b/browser/monacoWin.html
@@ -24,12 +24,12 @@
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 67327b938..d59c37532 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 dca52a16f..2f88ec1f3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
- "version": "1.11.4",
+ "version": "1.11.5",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@@ -30,13 +30,12 @@
"lint": "eslint",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix",
- "test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
+ "test": "pnpm buildStandalone && pnpm testTsc && pnpm lint && pnpm lint-styles && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"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",
@@ -56,7 +55,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 a49df467b..169d76fcf 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)
@@ -229,6 +226,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'}
@@ -241,10 +244,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':
@@ -259,6 +262,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'}
@@ -271,6 +280,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'}
@@ -283,6 +298,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'}
@@ -295,6 +316,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'}
@@ -307,6 +334,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'}
@@ -319,6 +352,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'}
@@ -331,6 +370,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'}
@@ -343,6 +388,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'}
@@ -355,10 +406,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':
@@ -373,6 +424,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'}
@@ -385,6 +442,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'}
@@ -397,6 +460,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'}
@@ -409,6 +478,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'}
@@ -421,6 +496,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'}
@@ -433,6 +514,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'}
@@ -445,12 +538,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'}
@@ -463,6 +568,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'}
@@ -475,6 +586,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'}
@@ -487,6 +604,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'}
@@ -499,6 +622,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'}
@@ -511,6 +640,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}
@@ -609,9 +744,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}
@@ -1205,131 +1337,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'}
@@ -1340,6 +1347,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'}
@@ -2889,13 +2901,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':
@@ -2904,55 +2919,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':
@@ -2961,75 +3000,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)
@@ -3135,8 +3216,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)
@@ -3936,91 +4015,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
@@ -4073,6 +4067,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 623f9f940..9c2b49708 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 deab86610..33168ff97 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 e88f1e2b9..920e59267 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 e3e28a14d..000000000
--- 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 24af628bd..9502d382e 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 c4c6d4705..63508eb06 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 3010327ec..08d2f8cac 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: true,
themeLinks: [],
+ eagerPatches: IS_REPORTER,
enabledThemes: [],
enableReactDevtools: false,
frameless: false,
diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx
index f3a8e1ddf..f930a40d2 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 7d80f425c..37ea4cc05 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 c7f8047db..9c06e3aa4 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 aef6ba3c8..2ca83b7fa 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 e20ca4b71..4456564cc 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 d552037fe..30920a067 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 f48f38e03..a9e34f784 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 5d5709f4b..c0a4cb920 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 a495907b2..4ac2e993d 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 3c5a324fd..0af611bc9 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 306ac737d..4a2688681 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 22a381360..aec7292a3 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 5699770ea..6760d4d06 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 28c371c5b..adca15d3b 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -100,6 +100,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 097c64560..b212e6241 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 e226c5ba1..4ff30b78c 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 c45b69964..9896b3a20 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 fe0668885..9ed1489c8 100644
--- a/src/webpack/common/utils.ts
+++ b/src/webpack/common/utils.ts
@@ -58,9 +58,9 @@ export const { match, P }: Pick = ma
export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep");
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 036c2a3fc..6f1fd25b8 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 1122d55bb..408d3b708 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 7ed791080..30180a7e9 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 000000000..ff28732c6
--- /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 db6d0918d..d2a42bd57 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": [