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