diff --git a/browser/manifest.json b/browser/manifest.json index 664f80ed..96fcdc57 100644 --- a/browser/manifest.json +++ b/browser/manifest.json @@ -43,7 +43,7 @@ { "resources": [ "dist/*", - "third-party/*" + "vendor/*" ], "matches": [ "*://*.discord.com/*" @@ -59,4 +59,4 @@ } ] } -} \ No newline at end of file +} 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 3565f938..adbe379a 100644 --- a/browser/monacoWin.html +++ b/browser/monacoWin.html @@ -126,4 +126,4 @@ - \ No newline at end of file + diff --git a/eslint.config.mjs b/eslint.config.mjs index 2379dcf8..fabceecd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -132,7 +132,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 1aef5e66..a590d38a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equicord", "private": "true", - "version": "1.11.4", + "version": "1.11.5", "description": "The other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": { @@ -31,7 +31,7 @@ "lint": "eslint", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint:fix": "pnpm lint --fix", - "test": "pnpm buildStandalone && pnpm lint:fix && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson", + "test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm generatePluginJson", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testTsc": "tsc --noEmit" }, @@ -68,7 +68,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-react": "^7.37.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25857e06..6a71ee39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,8 +108,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) @@ -274,6 +274,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.18': resolution: {integrity: sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==} engines: {node: '>=12'} @@ -286,10 +292,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.18': @@ -304,6 +310,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.18': resolution: {integrity: sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==} engines: {node: '>=12'} @@ -316,6 +328,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.18': resolution: {integrity: sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==} engines: {node: '>=12'} @@ -328,6 +346,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.18': resolution: {integrity: sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==} engines: {node: '>=12'} @@ -340,6 +364,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.18': resolution: {integrity: sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==} engines: {node: '>=12'} @@ -352,6 +382,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.18': resolution: {integrity: sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==} engines: {node: '>=12'} @@ -364,6 +400,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.18': resolution: {integrity: sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==} engines: {node: '>=12'} @@ -376,6 +418,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.18': resolution: {integrity: sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==} engines: {node: '>=12'} @@ -388,6 +436,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.18': resolution: {integrity: sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==} engines: {node: '>=12'} @@ -400,10 +454,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.18': @@ -418,6 +472,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.18': resolution: {integrity: sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==} engines: {node: '>=12'} @@ -430,6 +490,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.18': resolution: {integrity: sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==} engines: {node: '>=12'} @@ -442,6 +508,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.18': resolution: {integrity: sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==} engines: {node: '>=12'} @@ -454,6 +526,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.18': resolution: {integrity: sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==} engines: {node: '>=12'} @@ -466,6 +544,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.18': resolution: {integrity: sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==} engines: {node: '>=12'} @@ -478,6 +562,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.18': resolution: {integrity: sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==} engines: {node: '>=12'} @@ -490,12 +586,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.18': resolution: {integrity: sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==} engines: {node: '>=12'} @@ -508,6 +616,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.18': resolution: {integrity: sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==} engines: {node: '>=12'} @@ -520,6 +634,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.18': resolution: {integrity: sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==} engines: {node: '>=12'} @@ -532,6 +652,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.18': resolution: {integrity: sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==} engines: {node: '>=12'} @@ -544,6 +670,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.18': resolution: {integrity: sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==} engines: {node: '>=12'} @@ -556,6 +688,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.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1280,131 +1418,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.18: resolution: {integrity: sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==} engines: {node: '>=12'} @@ -1415,6 +1428,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.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -3014,13 +3032,16 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true + '@esbuild/aix-ppc64@0.25.0': + optional: true + '@esbuild/android-arm64@0.17.18': 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.18': @@ -3029,55 +3050,79 @@ snapshots: '@esbuild/android-arm@0.23.1': optional: true + '@esbuild/android-arm@0.25.0': + optional: true + '@esbuild/android-x64@0.17.18': optional: true '@esbuild/android-x64@0.23.1': optional: true + '@esbuild/android-x64@0.25.0': + optional: true + '@esbuild/darwin-arm64@0.17.18': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true + '@esbuild/darwin-arm64@0.25.0': + optional: true + '@esbuild/darwin-x64@0.17.18': optional: true '@esbuild/darwin-x64@0.23.1': optional: true + '@esbuild/darwin-x64@0.25.0': + optional: true + '@esbuild/freebsd-arm64@0.17.18': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true + '@esbuild/freebsd-arm64@0.25.0': + optional: true + '@esbuild/freebsd-x64@0.17.18': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true + '@esbuild/freebsd-x64@0.25.0': + optional: true + '@esbuild/linux-arm64@0.17.18': optional: true '@esbuild/linux-arm64@0.23.1': optional: true + '@esbuild/linux-arm64@0.25.0': + optional: true + '@esbuild/linux-arm@0.17.18': optional: true '@esbuild/linux-arm@0.23.1': optional: true + '@esbuild/linux-arm@0.25.0': + optional: true + '@esbuild/linux-ia32@0.17.18': 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.18': @@ -3086,75 +3131,117 @@ snapshots: '@esbuild/linux-loong64@0.23.1': optional: true + '@esbuild/linux-loong64@0.25.0': + optional: true + '@esbuild/linux-mips64el@0.17.18': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true + '@esbuild/linux-mips64el@0.25.0': + optional: true + '@esbuild/linux-ppc64@0.17.18': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true + '@esbuild/linux-ppc64@0.25.0': + optional: true + '@esbuild/linux-riscv64@0.17.18': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true + '@esbuild/linux-riscv64@0.25.0': + optional: true + '@esbuild/linux-s390x@0.17.18': optional: true '@esbuild/linux-s390x@0.23.1': optional: true + '@esbuild/linux-s390x@0.25.0': + optional: true + '@esbuild/linux-x64@0.17.18': 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.18': 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.18': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true + '@esbuild/openbsd-x64@0.25.0': + optional: true + '@esbuild/sunos-x64@0.17.18': optional: true '@esbuild/sunos-x64@0.23.1': optional: true + '@esbuild/sunos-x64@0.25.0': + optional: true + '@esbuild/win32-arm64@0.17.18': optional: true '@esbuild/win32-arm64@0.23.1': optional: true + '@esbuild/win32-arm64@0.25.0': + optional: true + '@esbuild/win32-ia32@0.17.18': optional: true '@esbuild/win32-ia32@0.23.1': optional: true + '@esbuild/win32-ia32@0.25.0': + optional: true + '@esbuild/win32-x64@0.17.18': optional: true '@esbuild/win32-x64@0.23.1': optional: true + '@esbuild/win32-x64@0.25.0': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))': dependencies: eslint: 9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4) @@ -4087,91 +4174,6 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - 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.18: optionalDependencies: '@esbuild/android-arm': 0.17.18 @@ -4224,6 +4226,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.1.2: {} escape-string-regexp@1.0.5: {} diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 4df96212..bc675503 100644 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -17,41 +17,44 @@ * along with this program. If not, see . */ -import esbuild from "esbuild"; +// @ts-check + import { createPackage } from "@electron/asar"; import { readdir, writeFile } from "fs/promises"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, watch } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues } from "./common.mjs"; -const defines = { - IS_STANDALONE: String(IS_STANDALONE), - IS_DEV: String(IS_DEV), - IS_REPORTER: String(IS_REPORTER), - IS_COMPANION_TEST: String(IS_COMPANION_TEST), - IS_UPDATER_DISABLED: String(IS_UPDATER_DISABLED), - IS_WEB: "false", - IS_EXTENSION: "false", - VERSION: JSON.stringify(VERSION), - BUILD_TIMESTAMP: String(BUILD_TIMESTAMP) -}; +const defines = stringifyValues({ + IS_STANDALONE, + IS_DEV, + IS_REPORTER, + IS_COMPANION_TEST, + IS_UPDATER_DISABLED, + IS_WEB: false, + IS_EXTENSION: false, + 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`; @@ -105,26 +108,28 @@ const globNativesPlugin = { } }; -await Promise.all([ +/** @type {import("esbuild").BuildOptions[]} */ +const buildConfigs = ([ // Discord Desktop main & renderer & preload - esbuild.build({ + { ...nodeCommonOpts, entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/main/index.ts")], outfile: "dist/desktop/patcher.js", footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") }, sourcemap, + plugins: [ + // @ts-ignore this is never undefined + ...nodeCommonOpts.plugins, + globNativesPlugin + ], define: { ...defines, IS_DISCORD_DESKTOP: "true", IS_VESKTOP: "false", IS_EQUIBOP: "false" - }, - plugins: [ - ...nodeCommonOpts.plugins, - globNativesPlugin - ] - }), - esbuild.build({ + } + }, + { ...commonOpts, entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/Vencord.ts")], outfile: "dist/desktop/renderer.js", @@ -143,8 +148,8 @@ await Promise.all([ IS_VESKTOP: "false", IS_EQUIBOP: "false" } - }), - esbuild.build({ + }, + { ...nodeCommonOpts, entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/preload.ts")], outfile: "dist/desktop/preload.js", @@ -156,27 +161,27 @@ await Promise.all([ IS_VESKTOP: "false", IS_EQUIBOP: "false" } - }), + }, - // Equicord Desktop main & renderer & preload - esbuild.build({ + // Vencord Desktop main & renderer & preload + { ...nodeCommonOpts, entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/main/index.ts")], outfile: "dist/equibop/main.js", footer: { js: "//# sourceURL=VencordMain\n" + sourceMapFooter("main") }, sourcemap, + plugins: [ + ...nodeCommonOpts.plugins, + globNativesPlugin + ], define: { ...defines, IS_DISCORD_DESKTOP: "false", IS_VESKTOP: "false", IS_EQUIBOP: "true" - }, - plugins: [ - ...nodeCommonOpts.plugins, - globNativesPlugin - ] - }), - esbuild.build({ + } + }, + { ...commonOpts, entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/Vencord.ts")], outfile: "dist/equibop/renderer.js", @@ -195,8 +200,8 @@ await Promise.all([ IS_VESKTOP: "false", IS_EQUIBOP: "true" } - }), - esbuild.build({ + }, + { ...nodeCommonOpts, entryPoints: [join(dirname(fileURLToPath(import.meta.url)), "../../src/preload.ts")], outfile: "dist/equibop/preload.js", @@ -208,14 +213,10 @@ await Promise.all([ IS_VESKTOP: "false", IS_EQUIBOP: "true" } - }), -]).catch(err => { - console.error("Build failed"); - console.error(err.message); - // make ci fail - if (!commonOpts.watch) - process.exitCode = 1; -}); + } +]); + +await buildOrWatchAll(buildConfigs); await Promise.all([ writeFile("dist/desktop/package.json", JSON.stringify({ diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 0db00773..9b27f2e7 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -17,42 +17,43 @@ * 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, IS_COMPANION_TEST, VERSION } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, 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"), - ...commonOpts.plugins + ...commonRendererPlugins ], - target: ["esnext"], - define: { - IS_WEB: "true", - IS_EXTENSION: "false", - IS_STANDALONE: "true", - IS_DEV: String(IS_DEV), - IS_REPORTER: String(IS_REPORTER), - IS_COMPANION_TEST: String(IS_COMPANION_TEST), - IS_DISCORD_DESKTOP: "false", - IS_VESKTOP: "false", - IS_EQUIBOP: "false", - IS_UPDATER_DISABLED: "true", - VERSION: JSON.stringify(VERSION), - BUILD_TIMESTAMP: String(BUILD_TIMESTAMP) - } + define: stringifyValues({ + IS_WEB: true, + IS_EXTENSION: false, + IS_STANDALONE: true, + IS_DEV, + IS_REPORTER, + IS_COMPANION_TEST, + IS_DISCORD_DESKTOP: false, + IS_VESKTOP: false, + IS_EQUIBOP: false, + IS_UPDATER_DISABLED: true, + VERSION, + BUILD_TIMESTAMP + }) }; const MonacoWorkerEntryPoints = [ @@ -67,58 +68,60 @@ const RnNoiseFiles = [ "LICENSE" ]; -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/browser/monaco" - }), - esbuild.build({ - entryPoints: ["browser/monaco.ts"], - bundle: true, - minify: true, - format: "iife", - outfile: "dist/browser/monaco/index.js", - loader: { - ".ttf": "file" - } - }), - esbuild.build({ - ...commonOptions, - outfile: "dist/browser/browser.js", - footer: { js: "//# sourceURL=VencordWeb" } - }), - esbuild.build({ - ...commonOptions, - outfile: "dist/browser/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});" - } - }) - ] -); + +/** @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/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 buildOrWatchAll(buildConfigs); /** * @type {(dir: string) => Promise} @@ -157,9 +160,9 @@ async function loadDir(dir, basePath = "") { */ async function buildExtension(target, files) { const entries = { - "dist/Vencord.js": await readFile("dist/browser/extension.js"), - "dist/Vencord.css": await readFile("dist/browser/extension.css"), - ...await loadDir("dist/browser/monaco"), + "dist/Vencord.js": await readFile("dist/extension.js"), + "dist/Vencord.css": await readFile("dist/extension.css"), + ...await loadDir("dist/vendor/monaco", "dist/"), ...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file => [`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)] ))), @@ -168,7 +171,7 @@ async function buildExtension(target, files) { 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 [ diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 32ad9be2..cfa46865 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"; @@ -29,8 +31,9 @@ import { fileURLToPath } from "url"; import { promisify } from "util"; import { getPluginTarget } from "../utils.mjs"; +import { builtinModules } from "module"; -const PackageJSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../package.json"))); +const PackageJSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf-8")); export const VERSION = PackageJSON.version; // https://reproducible-builds.org/docs/source-date-epoch/ @@ -56,6 +59,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 @@ -271,6 +302,18 @@ export const fileUrlPlugin = { } }; +/** + * @type {(filter: RegExp, message: string) => import("esbuild").Plugin} + */ +export const banImportPlugin = (filter, message) => ({ + name: "ban-imports", + setup: build => { + build.onResolve({ filter }, () => { + return { errors: [{ text: message }] }; + }); + } +}); + const styleModule = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "module/style.js"), "utf-8"); /** @@ -303,17 +346,26 @@ export const stylePlugin = { export const commonOpts = { logLevel: "info", bundle: true, - watch, minify: !watch && !IS_REPORTER, - sourcemap: watch ? "inline" : "", + sourcemap: watch ? "inline" : "external", legalComments: "linked", banner, plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin], external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"], inject: [join(dirname(fileURLToPath(import.meta.url)), "inject/react.mjs")], - jsxFactory: "VencordCreateElement", - jsxFragment: "VencordFragment", jsx: "transform", - // Work around https://github.com/evanw/esbuild/issues/2460 - tsconfig: join(dirname(fileURLToPath(import.meta.url)), "tsconfig.esbuild.json") + jsxFactory: "VencordCreateElement", + jsxFragment: "VencordFragment" }; + +const escapedBuiltinModules = builtinModules + .map(m => m.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) + .join("|"); + +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 3a8820bc..a709f7bc 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -16,11 +16,7 @@ * 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"; @@ -58,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; @@ -136,53 +135,67 @@ async function printReport() { console.log(); if (process.env.WEBHOOK_URL) { + const patchesToEmbed = (title: string, patches: PatchInfo[], color: number) => ({ + title, + color, + description: patches.map(p => { + const lines = [ + `**__${p.plugin} (${p.type}):__**`, + `ID: \`${p.id}\``, + `Match: ${toCodeBlock(p.match, "Match: ".length, true)}` + ]; + if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`); + + return lines.join("\n"); + }).join("\n\n"), + }); + + const embeds = [ + { + author: { + name: `Discord ${CANARY ? "Canary" : "Stable"} (${metaData.buildNumber})`, + url: `https://nelly.tools/builds/app/${metaData.buildHash}`, + icon_url: CANARY ? "https://cdn.discordapp.com/emojis/1252721945699549327.png?size=128" : "https://cdn.discordapp.com/emojis/1252721943463985272.png?size=128" + }, + color: CANARY ? 0xfbb642 : 0x5865f2 + }, + report.badPatches.length > 0 && patchesToEmbed("Bad Patches", report.badPatches, 0xff0000), + report.slowPatches.length > 0 && patchesToEmbed("Slow Patches", report.slowPatches, 0xf0b232), + report.badWebpackFinds.length > 0 && { + title: "Bad Webpack Finds", + description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None", + color: 0xff0000 + }, + report.badStarts.length > 0 && { + title: "Bad Starts", + description: report.badStarts.map(p => { + const lines = [ + `**__${p.plugin}:__**`, + toCodeBlock(p.error, 0, true) + ]; + return lines.join("\n"); + } + ).join("\n\n") || "None", + color: 0xff0000 + }, + report.otherErrors.length > 0 && { + title: "Discord Errors", + description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None", + color: 0xff0000 + } + ].filter(Boolean); + + if (embeds.length === 1) { + embeds.push({ + title: "No issues found", + description: "Seems like everything is working fine (for now)", + color: 0x00ff00 + }); + } + const body = JSON.stringify({ username: "Equicord Reporter" + (CANARY ? " (Canary)" : ""), - embeds: [ - { - author: { - name: `Discord ${CANARY ? "Canary" : "Stable"} (${metaData.buildNumber})`, - url: `https://nelly.tools/builds/app/${metaData.buildHash}`, - icon_url: CANARY ? "https://cdn.discordapp.com/emojis/1252721945699549327.png?size=128" : "https://cdn.discordapp.com/emojis/1252721943463985272.png?size=128" - }, - color: CANARY ? 0xfbb642 : 0x5865f2 - }, - { - title: "Bad Patches", - description: report.badPatches.map(p => { - const lines = [ - `**__${p.plugin} (${p.type}):__**`, - `ID: \`${p.id}\``, - `Match: ${toCodeBlock(p.match, "Match: ".length, true)}` - ]; - if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`); - return lines.join("\n"); - }).join("\n\n") || "None", - color: report.badPatches.length ? 0xff0000 : 0x00ff00 - }, - { - title: "Bad Webpack Finds", - description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None", - color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00 - }, - { - title: "Bad Starts", - description: report.badStarts.map(p => { - const lines = [ - `**__${p.plugin}:__**`, - toCodeBlock(p.error, 0, true) - ]; - return lines.join("\n"); - } - ).join("\n\n") || "None", - color: report.badStarts.length ? 0xff0000 : 0x00ff00 - }, - { - title: "Discord Errors", - description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None", - color: report.otherErrors.length ? 0xff0000 : 0x00ff00 - } - ] + embeds }); const headers = { @@ -249,14 +262,17 @@ page.on("console", async e => { switch (tag) { case "WebpackInterceptor:": - const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module|took [\d.]+?ms) \(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; 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, diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index 18996136..12e49d6d 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -27,7 +27,7 @@ import { openPluginModal } from "@components/PluginSettings/PluginModal"; import { AddonCard } from "@components/VencordSettings/AddonCard"; import { QuickAction, QuickActionCard } from "@components/VencordSettings/quickActions"; import { SettingsTab, wrapTab } from "@components/VencordSettings/shared"; -import { isPluginEnabled } from "@plugins"; +import { isPluginEnabled } from "@plugins/index"; import { openInviteModal } from "@utils/discord"; import { openModal } from "@utils/modal"; import { showItemInFolder } from "@utils/native"; diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx index 005783b9..943f0299 100644 --- a/src/components/VencordSettings/PatchHelperTab.tsx +++ b/src/components/VencordSettings/PatchHelperTab.tsx @@ -62,7 +62,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/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts index 21207855..9c06e3aa 100644 --- a/src/debug/loadLazyChunks.ts +++ b/src/debug/loadLazyChunks.ts @@ -8,7 +8,7 @@ import { Logger } from "@utils/Logger"; import { canonicalizeMatch } from "@utils/patches"; import * as Webpack from "@webpack"; import { wreq } from "@webpack"; -import { AnyModuleFactory, ModuleFactory } from "webpack"; +import { AnyModuleFactory, ModuleFactory } from "@webpack/wreq.d"; export async function loadLazyChunks() { const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); @@ -20,8 +20,7 @@ export async function loadLazyChunks() { const invalidChunks = new Set(); const deferredRequires = new Set(); - let chunksSearchingResolve: (value: void) => 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>; @@ -140,8 +139,8 @@ export async function loadLazyChunks() { } Webpack.factoryListeners.add(factoryListener); - for (const factoryId in wreq.m) { - factoryListener(wreq.m[factoryId]); + for (const moduleId in wreq.m) { + factoryListener(wreq.m[moduleId]); } await chunksSearchingDone; diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 1dccb42b..f76f583e 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -6,10 +6,10 @@ import { Logger } from "@utils/Logger"; import * as Webpack from "@webpack"; -import { addPatch, patches } from "plugins"; import { initWs } from "plugins/devCompanion.dev/initWs"; -import { getBuildNumber } from "webpack/patchWebpack"; +import { getBuildNumber, patchTimings } from "webpack/patchWebpack"; +import { addPatch, patches } from "../plugins"; import { loadLazyChunks } from "./loadLazyChunks"; import { reporterData } from "./reporterData"; @@ -19,8 +19,7 @@ async function runReporter() { try { ReporterLogger.log("Starting test..."); - let loadLazyChunksResolve: (value: void) => 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({ @@ -55,7 +54,7 @@ async function runReporter() { } } - for (const [plugin, moduleId, match, totalTime] of Vencord.WebpackPatcher.patchTimings) { + for (const [plugin, moduleId, match, totalTime] of patchTimings) { if (totalTime > 5) { new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`); } @@ -84,9 +83,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 diff --git a/src/equicordplugins/betterInvites/index.tsx b/src/equicordplugins/betterInvites/index.tsx index a2c650ba..d72cbb74 100644 --- a/src/equicordplugins/betterInvites/index.tsx +++ b/src/equicordplugins/betterInvites/index.tsx @@ -44,8 +44,8 @@ export default definePlugin({ replace: ",($1||((!$1)&&arguments[0].invite.expires_at)) && $2$self.RenderTip($1, $3, arguments[0].invite.expires_at)" }, { - match: /(\.jsx\)\(\i.\i.Info,{.+onClick):(\i\?\i:null),/, - replace: "$1:$2 || $self.Lurkable(arguments[0].invite.guild.id, arguments[0].invite.guild.features)," + match: /(\.jsx\)\(\i.\i.Info,{.+onClick:\i\?.{0,5}:null)/, + replace: "$& || $self.Lurkable(arguments[0].invite.guild.id, arguments[0].invite.guild.features)" }, { match: /(\.jsx\)\(\i\.\i\.Header,\{)text:(\i)/, @@ -61,6 +61,7 @@ export default definePlugin({ return
{(inviter && (currentUserId !== inviter.id)) ? <> openUserProfile(inviter.id)} src={inviter.avatar ? `https://cdn.discordapp.com/avatars/${inviter.id}/${inviter.avatar}.webp?size=80` : "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"} diff --git a/src/equicordplugins/customUserColors/SetColorModal.tsx b/src/equicordplugins/customUserColors/SetColorModal.tsx new file mode 100644 index 00000000..bdb36d6a --- /dev/null +++ b/src/equicordplugins/customUserColors/SetColorModal.tsx @@ -0,0 +1,92 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { set } from "@api/DataStore"; +import { classNameFactory } from "@api/Styles"; +import { Margins } from "@utils/margins"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; +import { findComponentByCodeLazy } from "@webpack"; +import { Button, Forms, useState } from "@webpack/common"; + +import { colors, DATASTORE_KEY } from "./index"; + +interface ColorPickerProps { + color: number; + showEyeDropper?: boolean; + suggestedColors?: string[]; + onChange(value: number | null): void; +} +const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)"); + +const cl = classNameFactory("vc-customColors-"); + +export function SetColorModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) { + const initialColor = parseInt(colors[userId], 16) || 372735; + // color picker default to current color set for user (if null it's 0x05afff :3 ) + + const [colorPickerColor, setColorPickerColor] = useState(initialColor); + // hex color code as an int (NOT rgb 0-255) + + + function setUserColor(color: number) { + setColorPickerColor(color); + } + + function handleKey(e: KeyboardEvent) { + if (e.key === "Enter") + saveUserColor(); + } + + async function saveUserColor() { + colors[userId] = colorPickerColor.toString(16).padStart(6, "0"); + await set(DATASTORE_KEY, colors); + modalProps.onClose(); + } + + async function deleteUserColor() { + delete colors[userId]; + await set(DATASTORE_KEY, colors); + modalProps.onClose(); + } + + return ( + + + + Custom Color + + + + +
+ + Pick a color + + +
+
+ + + + + +
+ ); +} diff --git a/src/equicordplugins/customUserColors/index.tsx b/src/equicordplugins/customUserColors/index.tsx new file mode 100644 index 00000000..16349162 --- /dev/null +++ b/src/equicordplugins/customUserColors/index.tsx @@ -0,0 +1,119 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { get } from "@api/DataStore"; +import { definePluginSettings, migratePluginSettings, Settings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import { openModal } from "@utils/modal"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; +import { extractAndLoadChunksLazy } from "@webpack"; +import { Menu } from "@webpack/common"; +import { User } from "discord-types/general"; + +import { SetColorModal } from "./SetColorModal"; + +export const DATASTORE_KEY = "equicord-customcolors"; +export let colors: Record = {}; + +(async () => { + colors = await get>(DATASTORE_KEY) || {}; +})(); + +// needed for color picker to be available without opening settings (ty pindms!!) +const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/); + +const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: { user: User; }) => { + if (user?.id == null) return; + + const setCustomColorItem = ( + { + await requireSettingsMenu(); + openModal(modalProps => ); + }} + /> + ); + + children.push(, setCustomColorItem); + +}; + +export function getCustomColorString(userId: string, withHash?: boolean): string | undefined { + if (!colors[userId] || !Settings.plugins.CustomUserColors.enabled) + return; + + if (withHash) + return `#${colors[userId]}`; + + return colors[userId]; +} + +const settings = definePluginSettings({ + dmList: { + type: OptionType.BOOLEAN, + description: "Users with custom colors defined will have their name in the dm list colored", + default: true, + }, + colorInServers: { + type: OptionType.BOOLEAN, + description: "If name colors should be changed within servers", + default: true, + } +}); + + +migratePluginSettings("CustomUserColors", "customUserColors"); +export default definePlugin({ + name: "CustomUserColors", + description: "Lets you add a custom color to any user, anywhere! Highly recommend to use with typingTweaks and roleColorEverywhere", + authors: [EquicordDevs.mochienya], + contextMenus: { "user-context": userContextMenuPatch }, + reporterTestable: ReporterTestable.None, + settings, + requireSettingsMenu, + getCustomColorString, + + patches: [ + { + // this also affects name headers in chats outside of servers + find: ".USERNAME),{", + replacement: { + match: /style:"username"===.{0,25}void 0/, + replace: "style:{color:$self.colorIfServer(arguments[0])}" + } + }, + { + predicate: () => settings.store.dmList, + find: "!1,wrapContent", + replacement: { + match: /(nameAndDecorators,)/, + replace: "$1style:{color:$self.colorDMList(arguments[0])}," + }, + }, + ], + + colorDMList(a: any): string | undefined { + const userId = a?.subText?.props?.user?.id; + if (!userId) return; + const colorString = getCustomColorString(userId, true); + if (colorString) return colorString; + return "inherit"; + }, + + colorIfServer(a: any): string | undefined { + const roleColor = a.author?.colorString ?? "inherit"; + + if (a.channel.guild_id && !settings.store.colorInServers) return roleColor; + + const color = getCustomColorString(a.message.author.id, true); + return color ?? roleColor; + } +}); diff --git a/src/equicordplugins/customUserColors/styles.css b/src/equicordplugins/customUserColors/styles.css new file mode 100644 index 00000000..d48407d3 --- /dev/null +++ b/src/equicordplugins/customUserColors/styles.css @@ -0,0 +1,16 @@ +.vc-customColors-modal-header { + place-content: center; + justify-content: space-between; +} + +.vc-customColors-modal-header h1 { + margin: 0; +} + +.vc-customColors-modal-content { + padding: 1em; +} + +.vc-customColors-modal-footer { + gap: 16px; +} diff --git a/src/equicordplugins/fixFileExtensions/components.tsx b/src/equicordplugins/fixFileExtensions/components.tsx deleted file mode 100644 index 3c9322f2..00000000 --- a/src/equicordplugins/fixFileExtensions/components.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -const extensionMap = { - "ogg": [".ogv", ".oga", ".ogx", ".ogm", ".spx", ".opus"], - "jpg": [".jpg", ".jpeg", ".jfif", ".jpe", ".jif", ".jfi", ".pjpeg", ".pjp"], - "svg": [".svgz"], - "mp4": [".m4v", ".m4r", ".m4p"], - "m4a": [".m4b"], - "mov": [".movie", ".qt"], -}; - -export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [target, exts]) => { - exts.forEach(ext => acc[ext] = `.${target}`); - return acc; -}, {} as Record); diff --git a/src/equicordplugins/fixFileExtensions/index.tsx b/src/equicordplugins/fixFileExtensions/index.tsx index 7e1b646a..18df49d4 100644 --- a/src/equicordplugins/fixFileExtensions/index.tsx +++ b/src/equicordplugins/fixFileExtensions/index.tsx @@ -9,7 +9,19 @@ import { Settings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { ReporterTestable } from "@utils/types"; -import { reverseExtensionMap } from "./components"; +const extensionMap = { + "ogg": [".ogv", ".oga", ".ogx", ".ogm", ".spx", ".opus"], + "jpg": [".jpg", ".jpeg", ".jfif", ".jpe", ".jif", ".jfi", ".pjpeg", ".pjp"], + "svg": [".svgz"], + "mp4": [".m4v", ".m4r", ".m4p"], + "m4a": [".m4b"], + "mov": [".movie", ".qt"], +}; + +export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [target, exts]) => { + exts.forEach(ext => acc[ext] = `.${target}`); + return acc; +}, {} as Record); type ExtUpload = Upload & { fixExtension?: boolean; }; diff --git a/src/equicordplugins/FontLoader/index.tsx b/src/equicordplugins/fontLoader/index.tsx similarity index 72% rename from src/equicordplugins/FontLoader/index.tsx rename to src/equicordplugins/fontLoader/index.tsx index a934acfb..283b9adf 100644 --- a/src/equicordplugins/FontLoader/index.tsx +++ b/src/equicordplugins/fontLoader/index.tsx @@ -13,6 +13,7 @@ import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { Card, Forms, React, TextInput } from "@webpack/common"; + interface GoogleFontMetadata { family: string; displayName: string; @@ -27,7 +28,15 @@ interface GoogleFontMetadata { }>; }>; } -const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko) Chrome / 128.0.0.0 Safari / 537.36"; + +const createGoogleFontUrl = (family: string, options = "") => + `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}${options}&display=swap`; + +const loadFontStyle = (url: string) => { + document.head.insertAdjacentHTML("beforeend", ``); + return document.createElement("style"); +}; + async function searchGoogleFonts(query: string) { try { const response = await fetch("https://fonts.google.com/$rpc/fonts.fe.catalog.actions.metadata.MetadataService/FontSearch", { @@ -36,14 +45,12 @@ async function searchGoogleFonts(query: string) { "content-type": "application/json+protobuf", "x-user-agent": "grpc-web-javascript/0.1" }, - // the nulls are optional filters body: JSON.stringify([[query, null, null, null, null, null, 1], [5], null, 16]) }); const data = await response.json(); if (!data?.[1]) return []; - // god please help me - const fonts = data[1].map(([_, fontData]: [string, any[]]) => ({ + return data[1].map(([_, fontData]: [string, any[]]) => ({ family: fontData[0], displayName: fontData[1], authors: fontData[2], @@ -54,81 +61,52 @@ async function searchGoogleFonts(query: string) { })) })) })); - return fonts; - // LETS GO IT FUCKING WORKSSSSSSSSSSSS } catch (err) { console.error("Failed to fetch fonts:", err); return []; } } -async function preloadFont(family: string) { - // https://developers.google.com/fonts/docs/css2 - const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}&text=The quick brown fox jumps over the lazy dog&display=swap`; - const css = await fetch(url, { - headers: { - "User-Agent": userAgent - } - }).then(r => r.text()); +const preloadFont = (family: string) => + loadFontStyle(createGoogleFontUrl(family, "&text=The quick brown fox jumps over the lazy dog")); - const style = document.createElement("style"); - style.textContent = css; - document.head.appendChild(style); - return style; -} +let styleElement: HTMLStyleElement | null = null; -async function applyFont(fontFamily: string) { +const applyFont = async (fontFamily: string) => { if (!fontFamily) { - if (styleElement) { - styleElement.remove(); - styleElement = null; - } + styleElement?.remove(); + styleElement = null; return; } try { - const response = await fetch( - `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@300;400;500;600;700&display=swap`, - { - headers: { - "User-Agent": userAgent - } - } - ); - const css = await response.text(); - if (!styleElement) { styleElement = document.createElement("style"); document.head.appendChild(styleElement); } + loadFontStyle(createGoogleFontUrl(fontFamily, ":wght@300;400;500;600;700")); styleElement.textContent = ` - ${css} - * { - --font-primary: '${fontFamily}', sans-serif !important; - --font-display: '${fontFamily}', sans-serif !important; - --font-headline: '${fontFamily}', sans-serif !important; - --font-code: '${fontFamily}', monospace !important; - } - `; + * { + --font-primary: '${fontFamily}', sans-serif !important; + --font-display: '${fontFamily}', sans-serif !important; + --font-headline: '${fontFamily}', sans-serif !important; + --font-code: '${fontFamily}', monospace !important; + } + `; } catch (err) { console.error("Failed to load font:", err); } -} +}; function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) => void; }) { const [query, setQuery] = React.useState(""); const [results, setResults] = React.useState([]); const [loading, setLoading] = React.useState(false); - - const previewStyles = React.useRef([]); - - React.useEffect(() => { - return () => { - previewStyles.current.forEach(style => style.remove()); - }; + React.useEffect(() => () => { + previewStyles.current.forEach(style => style.remove()); }, []); const debouncedSearch = debounce(async (value: string) => { @@ -138,22 +116,18 @@ function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) = setLoading(false); return; } + const fonts = await searchGoogleFonts(value); - previewStyles.current.forEach(style => style.remove()); - previewStyles.current = []; - - const styles = await Promise.all(fonts.map(f => preloadFont(f.family))); - previewStyles.current = styles; - + previewStyles.current = await Promise.all(fonts.map(f => preloadFont(f.family))); setResults(fonts); setLoading(false); }, 300); - const handleSearch = React.useCallback((value: string) => { - setQuery(value); - debouncedSearch(value); - }, []); + const handleSearch = (e: string) => { + setQuery(e); + debouncedSearch(e); + }; return ( @@ -193,8 +167,6 @@ function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) = ); } -let styleElement: HTMLStyleElement | null = null; - const settings = definePluginSettings({ selectedFont: { type: OptionType.STRING, diff --git a/src/equicordplugins/FontLoader/styles.css b/src/equicordplugins/fontLoader/styles.css similarity index 100% rename from src/equicordplugins/FontLoader/styles.css rename to src/equicordplugins/fontLoader/styles.css diff --git a/src/equicordplugins/glide/index.tsx b/src/equicordplugins/glide/index.tsx index f195800b..5547133a 100644 --- a/src/equicordplugins/glide/index.tsx +++ b/src/equicordplugins/glide/index.tsx @@ -797,5 +797,5 @@ export default definePlugin({ }, startAt: StartAt.DOMContentLoaded, // preview thing, kinda low effort but eh - settingsAboutComponent: () => + settingsAboutComponent: () => }); diff --git a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx index 5d8c95a9..4a98b920 100644 --- a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx +++ b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx @@ -275,7 +275,7 @@ function SubmitThemes() { color: "var(--text-normal)" }}>

This tab was replaced in favour of the new website:

-

discord-themes.com

+

discord-themes.com

{users.map(user => ( - + {getUsername(user)} ))} diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts index 58d8d42a..30920a06 100644 --- a/src/plugins/_core/noTrack.ts +++ b/src/plugins/_core/noTrack.ts @@ -20,7 +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"; +import { WebpackRequire } from "@webpack/wreq.d"; const settings = definePluginSettings({ disableAnalytics: { diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 2bac75ef..c18c374f 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -164,6 +164,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/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index 6c315460..a903efa0 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -19,12 +19,11 @@ import { Upload } from "@api/MessageEvents"; import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; +import { reverseExtensionMap } from "@equicordplugins/fixFileExtensions"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByCodeLazy, findByPropsLazy } from "@webpack"; -import { reverseExtensionMap } from "../../equicordplugins/fixFileExtensions/components"; - type AnonUpload = Upload & { anonymise?: boolean; }; const ActionBarIcon = findByCodeLazy(".actionBarIcon)"); diff --git a/src/plugins/devCompanion.dev/initWs.tsx b/src/plugins/devCompanion.dev/initWs.tsx index 0d22dfc5..36ee4778 100644 --- a/src/plugins/devCompanion.dev/initWs.tsx +++ b/src/plugins/devCompanion.dev/initWs.tsx @@ -337,7 +337,7 @@ export 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 68aacc6b..e5594e99 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -31,6 +31,7 @@ import { Logger } from "@utils/Logger"; 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; diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 77fa2784..e4e9c209 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -17,6 +17,7 @@ */ import { definePluginSettings } from "@api/Settings"; +import { getUserSettingLazy } from "@api/UserSettings"; import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; @@ -81,6 +82,8 @@ const enum NameFormat { AlbumName = "album" } +const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!; + const applicationId = "1108588077900898414"; const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; @@ -129,6 +132,11 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: false, }, + enableGameActivity: { + description: "Enable game activity for last.fm", + type: OptionType.BOOLEAN, + default: false, + }, statusName: { description: "custom status text", type: OptionType.STRING, @@ -293,6 +301,11 @@ export default definePlugin({ } const trackData = await this.fetchTrackData(); + if (settings.store.enableGameActivity && trackData) { + ShowCurrentGame.updateSetting(true); + } else if (settings.store.enableGameActivity) { + ShowCurrentGame.updateSetting(false); + } if (!trackData) return null; const largeImage = this.getLargeImage(trackData); diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index 7b7bcc44..d00016a0 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -16,9 +16,10 @@ * along with this program. If not, see . */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { makeRange } from "@components/PluginSettings/components"; +import { getCustomColorString } from "@equicordplugins/customUserColors"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; @@ -164,6 +165,9 @@ export default definePlugin({ getColorString(userId: string, channelOrGuildId: string) { try { + if (Settings.plugins.customUserColors.enabled) + return getCustomColorString(userId, true); + const guildId = ChannelStore.getChannel(channelOrGuildId)?.guild_id ?? GuildStore.getGuild(channelOrGuildId)?.id; if (guildId == null) return null; diff --git a/src/plugins/showAllMessageButtons/index.ts b/src/plugins/showAllMessageButtons/index.ts index 1d689013..10069a58 100644 --- a/src/plugins/showAllMessageButtons/index.ts +++ b/src/plugins/showAllMessageButtons/index.ts @@ -1,36 +1,65 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 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) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import { definePluginSettings } from "@api/Settings"; +import { Devs, EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { MessageActions } from "@webpack/common"; + +const settings = definePluginSettings({ + noShiftDelete: { + type: OptionType.BOOLEAN, + description: "Remove requirement to hold shift for deleting a message.", + default: true, + }, + noShiftPin: { + type: OptionType.BOOLEAN, + description: "Remove requirement to hold shift for pinning a message.", + default: true, + }, +}); + +const PinActions = findByPropsLazy("pinMessage", "unpinMessage"); export default definePlugin({ name: "ShowAllMessageButtons", description: "Always show all message buttons no matter if you are holding the shift key or not.", - authors: [Devs.Nuckyz], + authors: [Devs.Nuckyz, EquicordDevs.mochienya], + settings, patches: [ { find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}", - replacement: { - match: /isExpanded:\i&&(.+?),/, - replace: "isExpanded:$1," - } - } - ] + replacement: [ + { + match: /isExpanded:\i&&(.+?),/, + replace: "isExpanded:$1," + }, + { + predicate: () => settings.store.noShiftDelete, + match: /onClick:.{10,20}(?=,dangerous:!0)/, + replace: "onClick:() => $self.deleteMessage(arguments[0].message)", + }, + { + predicate: () => settings.store.noShiftPin, + match: /onClick:.{10,30}(?=\},"pin")/, + replace: "onClick:() => $self.toggleMessagePin(arguments[0])," + } + ] + }, + ], + + deleteMessage({ channel_id, id }) { + MessageActions.deleteMessage(channel_id, id); + }, + toggleMessagePin({ channel, message }) { + if (message.pinned) + return PinActions.unpinMessage(channel, message.id); + + PinActions.pinMessage(channel, message.id); + }, }); diff --git a/src/plugins/typingTweaks/index.tsx b/src/plugins/typingTweaks/index.tsx index ff68a486..7991b058 100644 --- a/src/plugins/typingTweaks/index.tsx +++ b/src/plugins/typingTweaks/index.tsx @@ -16,8 +16,9 @@ * along with this program. If not, see . */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; +import { getCustomColorString } from "@equicordplugins/customUserColors"; import { Devs } from "@utils/constants"; import { openUserProfile } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; @@ -57,6 +58,12 @@ interface Props { guildId: string; } +function typingUserColor(guildId: string, userId: string) { + if (!settings.store.showRoleColors) return; + if (Settings.plugins.customUserColors.enabled) return getCustomColorString(userId, true); + return GuildMemberStore.getMember(guildId, userId)?.colorString; +} + const TypingUser = ErrorBoundary.wrap(function ({ user, guildId }: Props) { return ( diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index f08cf344..0d936e8d 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -61,9 +61,9 @@ export const { match, P }: Pick = ma export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep"); export const i18n = mapMangledModuleLazy('defaultLocale:"en-US"', { + t: filters.byProps(runtimeHashMessageKey("DISCORD")), intl: filters.byProps("string", "format"), - t: filters.byProps(runtimeHashMessageKey("DISCORD")) -}); +}, true); export let SnowflakeUtils: t.SnowflakeUtils; waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 7d6b1676..707c123c 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -9,33 +9,25 @@ 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 { Patch, PatchReplacement } from "@utils/types"; import { reporterData } from "debug/reporterData"; import { traceFunctionWithResults } from "../debug/Tracer"; -import { patches } from "../plugins"; -import { _initWebpack, _shouldIgnoreModule, AnyModuleFactory, AnyWebpackRequire, factoryListeners, findModuleId, MaybeWrappedModuleFactory, ModuleExports, moduleListeners, waitForSubscriptions, WebpackRequire, WrappedModuleFactory, wreq } from "."; +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"); -/** A set with all the Webpack instances */ export const allWebpackInstances = new Set(); -export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: string | RegExp, totalTime: number]>; -const logger = new Logger("WebpackInterceptor", "#8caaee"); -/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */ -let wreqFallbackApplied = false; -/** Whether we should be patching factories. - * - * This should be disabled if we start searching for the module to get the build number, and then resumed once it's done. - * */ -let shouldPatchFactories = true; +export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: PatchReplacement["match"], totalTime: number]>; export const getBuildNumber = makeLazy(() => { try { - shouldPatchFactories = false; - try { if (wreq.m[128014]?.toString().includes("Trying to open a changelog for an invalid build number")) { const hardcodedGetBuildNumber = wreq(128014).b as () => number; @@ -60,13 +52,23 @@ export const getBuildNumber = makeLazy(() => { return typeof buildNumber === "number" ? buildNumber : -1; } catch { return -1; - } finally { - shouldPatchFactories = true; } }); -type Define = typeof Reflect.defineProperty; -const define: Define = (target, p, attributes) => { +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"); + +/** Whether we tried to fallback to the WebpackRequire of the factory, or disabled patches */ +let wreqFallbackApplied = false; + +const define: typeof Reflect.defineProperty = (target, p, attributes) => { if (Object.hasOwn(attributes, "value")) { attributes.writable = true; } @@ -78,22 +80,18 @@ const define: Define = (target, p, attributes) => { }); }; -export function getOriginalFactory(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { - const moduleFactory = webpackRequire.m[id]; - return (moduleFactory?.[SYM_ORIGINAL_FACTORY] ?? moduleFactory) as AnyModuleFactory | undefined; -} +// 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. -export function getFactoryPatchedSource(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { - return webpackRequire.m[id]?.[SYM_PATCHED_SOURCE]; -} +// 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. -export function getFactoryPatchedBy(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { - return webpackRequire.m[id]?.[SYM_PATCHED_BY]; -} +// 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 the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push -// We use this setter to intercept when wreq.m is defined and apply the patching in its module factories. -// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or defining getters for the patched versions. +// 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", { @@ -102,7 +100,7 @@ define(Function.prototype, "m", { set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) { define(this, "m", { value: originalModules }); - // Ensure this is one of Discord main Webpack instances. + // Ensure this is likely one of Discord main Webpack instances. // We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here. const { stack } = new Error(); if (!stack?.includes("http") || stack.match(/at \d+? \(/) || !String(this).includes("exports:{}")) { @@ -110,53 +108,108 @@ define(Function.prototype, "m", { } const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); - allWebpackInstances.add(this); - - // Define a setter for the ensureChunk property of WebpackRequire. Only the main Webpack (which is the only that includes chunk loading) has this property. - // So if the setter is called, this means we can initialize the internal references to WebpackRequire. - define(this, "e", { + // 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, - set(this: WebpackRequire, ensureChunk: WebpackRequire["e"]) { - define(this, "e", { value: ensureChunk }); - clearTimeout(setterTimeout); + set(this: AnyWebpackRequire, bundlePath: NonNullable) { + define(this, "p", { value: bundlePath }); + clearTimeout(bundlePathTimeout); - logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); - _initWebpack(this); + if (bundlePath !== "/assets/") { + return; + } + + patchThisInstance(); + + 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.e will always be set before the timeout runs. - const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "e"), 0); - // Patch the pre-populated factories - for (const id in originalModules) { - if (updateExistingFactory(originalModules, id, originalModules[id], true)) { - continue; + // 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(); + } + } + }); } - - notifyFactoryListeners(originalModules[id]); - defineModulesFactoryGetter(id, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]); - } - - define(originalModules, Symbol.toStringTag, { - value: "ModuleFactories", - enumerable: false }); - // The proxy responsible for patching the module factories when they are set, or defining getters for the patched versions - 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)); - */ + // 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); - define(this, "m", { value: proxiedModuleFactories }); + /** + * Patch the current Webpack instance assigned to `this` context. + * This should only be called if this instance was later found to be one we need to patch. + */ + const patchThisInstance = () => { + logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); + allWebpackInstances.add(this); + + // Proxy (and maybe patch) pre-populated factories + for (const moduleId in originalModules) { + updateExistingOrProxyFactory(originalModules, moduleId, originalModules[moduleId], originalModules, true); + } + + define(originalModules, Symbol.toStringTag, { + value: "ModuleFactories", + enumerable: false + }); + + const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler); + /* + If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype + Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler)); + */ + + define(this, "m", { value: proxiedModuleFactories }); + + // Overwrite Webpack's defineExports function to define the export descriptors configurable. + // This is needed so we can later blacklist specific exports from Webpack search by making them non-enumerable + this.d = function (exports: 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], + }); + } + } + }; + }; } }); +// 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 @@ -173,71 +226,132 @@ const moduleFactoriesHandler: ProxyHandler = { }, */ - // The set trap for patching or defining getters for the module factories when new module factories are loaded - set(target, p, newValue, receiver) { - if (updateExistingFactory(target, p, newValue)) { + 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); + } + + // 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; } - notifyFactoryListeners(newValue); - defineModulesFactoryGetter(p, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(p, newValue) : newValue); + const originalFactory: AnyModuleFactory = target[SYM_ORIGINAL_FACTORY] ?? target; - return true; + // Redirect these properties to the original factory, including making `toString` return the original factory `toString` + if (p === "toString" || p === SYM_PATCHED_SOURCE || p === SYM_PATCHED_BY) { + const v = Reflect.get(originalFactory, p, originalFactory); + return p === "toString" ? v.bind(originalFactory) : v; + } + + return Reflect.get(target, p, receiver); } }; +function updateExistingOrProxyFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget = false) { + if (updateExistingFactory(moduleFactories, moduleId, newFactory, receiver, ignoreExistingInTarget)) { + return true; + } + + notifyFactoryListeners(moduleId, newFactory); + + const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, newFactory) : newFactory, moduleFactoryHandler); + return Reflect.set(moduleFactories, moduleId, proxiedFactory, receiver); +} + /** - * Update a factory that exists in any Webpack instance with a new original factory. + * Update a duplicated factory that exists in any of the Webpack instances we track with a new original factory. * - * @target The module factories where this new original factory is being set - * @param id The id of the module + * @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 ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget - * @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance + * @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(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) { - let existingFactory: TypedPropertyDescriptor | undefined; +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 === moduleFactoriesTarget) continue; + if (ignoreExistingInTarget && wreq.m === moduleFactories) { + continue; + } - if (Object.hasOwn(wreq.m, id)) { - existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id); + if (Object.hasOwn(wreq.m, moduleId)) { + existingFactory = wreq.m[moduleId]; moduleFactoriesWithFactory = wreq.m; break; } } if (existingFactory != null) { - // If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required. - // So define the descriptor of it on this current Webpack instance (if it doesn't exist already), call Reflect.set with the new original, - // and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) - - if (moduleFactoriesWithFactory !== moduleFactoriesTarget) { - Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory); + // Sanity check to make sure these factories are equal + if (String(newFactory) !== String(existingFactory)) { + return false; } - // Persist patched source and patched by in the new original factory, if the patched one has already been required - if (IS_DEV && existingFactory.value != null) { - newFactory[SYM_PATCHED_SOURCE] = existingFactory.value[SYM_PATCHED_SOURCE]; - newFactory[SYM_PATCHED_BY] = existingFactory.value[SYM_PATCHED_BY]; + // 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); } - return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget); + // 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; + } + + // Persist patched source and patched by in the new original factory + if (IS_DEV) { + newFactory[SYM_PATCHED_SOURCE] = existingFactory[SYM_PATCHED_SOURCE]; + newFactory[SYM_PATCHED_BY] = existingFactory[SYM_PATCHED_BY]; + } + + return true; } return false; } +/** + * Define a module factory in all the Webpack instances we track. + * + * @param moduleId The id of the module + * @param factory The factory + */ +function defineInWebpackInstances(moduleId: PropertyKey, factory: AnyModuleFactory) { + for (const wreq of allWebpackInstances) { + define(wreq.m, moduleId, { value: factory }); + } +} + /** * Notify all factory listeners. * + * @param moduleId The id of the module * @param factory The original factory to notify for */ -function notifyFactoryListeners(factory: AnyModuleFactory) { +function notifyFactoryListeners(moduleId: PropertyKey, factory: AnyModuleFactory) { for (const factoryListener of factoryListeners) { try { - factoryListener(factory); + factoryListener(factory, moduleId); } catch (err) { logger.error("Error in Webpack factory listener:\n", err, factoryListener); } @@ -245,190 +359,124 @@ function notifyFactoryListeners(factory: AnyModuleFactory) { } /** - * Define the getter for returning the patched version of the module factory. + * Run a (possibly) patched module factory with a wrapper which notifies our listeners. * - * If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original - * and only be patched when accessed for the first time. - * - * @param id The id of the module - * @param factory The original or patched module factory + * @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 defineModulesFactoryGetter(id: PropertyKey, factory: MaybeWrappedModuleFactory) { - const descriptor: PropertyDescriptor = { - get() { - // SYM_ORIGINAL_FACTORY means the factory is already patched - if (!shouldPatchFactories || factory[SYM_ORIGINAL_FACTORY] != null) { - return factory; - } +function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unknown, argArray: Parameters) { + const originalFactory = patchedFactory[SYM_ORIGINAL_FACTORY]; - return (factory = wrapAndPatchFactory(id, factory)); - }, - set(newFactory: MaybeWrappedModuleFactory) { - if (IS_DEV) { - newFactory[SYM_PATCHED_SOURCE] = factory[SYM_PATCHED_SOURCE]; - newFactory[SYM_PATCHED_BY] = factory[SYM_PATCHED_BY]; - } - - if (factory[SYM_ORIGINAL_FACTORY] != null) { - factory.toString = newFactory.toString.bind(newFactory); - factory[SYM_ORIGINAL_FACTORY] = newFactory; - } else { - factory = newFactory; - } - } - }; - - // Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object - // have the patched version - for (const wreq of allWebpackInstances) { - define(wreq.m, id, descriptor); + if (patchedFactory === originalFactory) { + // @ts-expect-error Clear up ORIGINAL_FACTORY if the factory did not have any patch applied + delete patchedFactory[SYM_ORIGINAL_FACTORY]; } -} -/** - * Wraps and patches a module factory. - * - * @param id The id of the module - * @param factory The original or patched module factory - * @returns The wrapper for the patched module factory - */ -function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) { - const [patchedFactory, patchedSource, patchedBy] = patchFactory(id, originalFactory); + let [module, exports, require] = argArray; - const wrappedFactory: WrappedModuleFactory = function (...args) { - // Restore the original factory in all the module factories objects. We want to make sure the original factory is restored properly, no matter what is the Webpack instance - for (const wreq of allWebpackInstances) { - define(wreq.m, id, { value: wrappedFactory[SYM_ORIGINAL_FACTORY] }); - } + // Restore the original factory in all the module factories objects, discarding our proxy and allowing it to be garbage collected + defineInWebpackInstances(module.id, originalFactory); - // eslint-disable-next-line prefer-const - let [module, exports, require] = args; + if (wreq == null) { + if (!wreqFallbackApplied) { + wreqFallbackApplied = true; - 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]; - // Make sure the require argument is actually the WebpackRequire function - if (typeof require === "function" && require.m != 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}` + + ")" + ); - logger.warn( - "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + - `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + - ")" - ); - - _initWebpack(require as WebpackRequire); - } else if (IS_DEV) { - logger.error("WebpackRequire was not initialized, running modules without patches instead."); - return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args); - } + // Could technically be wrong, but it's better than nothing + _initWebpack(require as WebpackRequire); } else if (IS_DEV) { - return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args); + logger.error("WebpackRequire was not initialized, running modules without patches instead."); + return originalFactory.apply(thisArg, argArray); } + } else if (IS_DEV) { + return originalFactory.apply(thisArg, argArray); + } + } + + let factoryReturn: unknown; + try { + factoryReturn = patchedFactory.apply(thisArg, argArray); + } catch (err) { + // Just re-throw Discord errors + if (patchedFactory === originalFactory) { + throw err; } - let factoryReturn: unknown; - try { - // Call the patched factory - factoryReturn = patchedFactory.apply(this, args); - } catch (err) { - // Just re-throw Discord errors - if (patchedFactory === wrappedFactory[SYM_ORIGINAL_FACTORY]) { - throw err; - } + logger.error("Error in patched module factory:\n", err); + return originalFactory.apply(thisArg, argArray); + } - logger.error("Error in patched module factory:\n", err); - return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args); - } + exports = module.exports; + if (exports == null) { + return factoryReturn; + } - exports = module.exports; - if (exports == null) { + if (typeof require === "function" && require.c) { + if (_blacklistBadModules(require.c, exports, module.id)) { return factoryReturn; } - - if (typeof require === "function") { - const shouldIgnoreModule = _shouldIgnoreModule(exports); - - if (shouldIgnoreModule) { - if (require.c != null) { - Object.defineProperty(require.c, id, { - value: require.c[id], - enumerable: false, - configurable: true, - writable: true - }); - } - - return factoryReturn; - } - } - - for (const callback of moduleListeners) { - try { - callback(exports, id); - } catch (err) { - logger.error("Error in Webpack module listener:\n", err, callback); - } - } - - for (const [filter, callback] of waitForSubscriptions) { - try { - if (filter(exports)) { - waitForSubscriptions.delete(filter); - callback(exports, id); - continue; - } - - if (typeof exports !== "object") { - continue; - } - - for (const exportKey in exports) { - const exportValue = exports[exportKey]; - - if (exportValue != null && filter(exportValue)) { - waitForSubscriptions.delete(filter); - callback(exportValue, id); - break; - } - } - } catch (err) { - logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback); - } - } - - return factoryReturn; - }; - - wrappedFactory.toString = originalFactory.toString.bind(originalFactory); - wrappedFactory[SYM_ORIGINAL_FACTORY] = originalFactory; - - if (IS_DEV && patchedFactory !== originalFactory) { - wrappedFactory[SYM_PATCHED_SOURCE] = patchedSource; - wrappedFactory[SYM_PATCHED_BY] = patchedBy; - originalFactory[SYM_PATCHED_SOURCE] = patchedSource; - originalFactory[SYM_PATCHED_BY] = patchedBy; } - // @ts-expect-error Allow GC to get into action, if possible - originalFactory = undefined; - return wrappedFactory; + for (const callback of moduleListeners) { + try { + callback(exports, module.id); + } catch (err) { + logger.error("Error in Webpack module listener:\n", err, callback); + } + } + + for (const [filter, callback] of waitForSubscriptions) { + try { + if (filter(exports)) { + waitForSubscriptions.delete(filter); + callback(exports, module.id); + continue; + } + + if (typeof exports !== "object") { + continue; + } + + for (const exportKey in exports) { + const exportValue = exports[exportKey]; + + if (exportValue != null && filter(exportValue)) { + waitForSubscriptions.delete(filter); + callback(exportValue, module.id); + break; + } + } + } catch (err) { + logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback); + } + } + + return factoryReturn; } /** * Patches a module factory. * - * @param id The id of the module - * @param factory The original module factory - * @returns The patched module factory, the patched source of it, and the plugins that patched it + * @param moduleId The id of the module + * @param originalFactory The original module factory + * @returns The patched module factory */ -function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFactory: AnyModuleFactory, patchedSource: string, patchedBy: Set] { +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(factory); + let code: string = "0," + String(originalFactory); let patchedSource = code; - let patchedFactory = factory; + let patchedFactory = originalFactory; const patchedBy = new Set(); @@ -443,8 +491,8 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto continue; } - // Reporter eagerly patches and cannot retrieve the build number because this code runs before the module for it is loaded - const buildNumber = IS_REPORTER ? -1 : getBuildNumber(); + // 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 ( @@ -464,7 +512,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto }); const previousCode = code; - const previousFactory = factory; + const previousFactory = originalFactory; let markedAsPatched = false; // We change all patch.replacement to array in plugins/index @@ -483,25 +531,25 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto } const lastCode = code; - const lastFactory = factory; + const lastFactory = originalFactory; try { const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string); if (IS_REPORTER) { - patchTimings.push([patch.plugin, id, replacement.match, totalTime]); + 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(id)}): ${replacement.match}`); + 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 (IS_COMPANION_TEST) reporterData.failedPatches.hadNoEffect.push({ ...patch, - id + id: moduleId }); } @@ -517,7 +565,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto if (IS_COMPANION_TEST) reporterData.failedPatches.undoingPatchGroup.push({ ...patch, - id + id: moduleId }); break; @@ -527,7 +575,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto } code = newCode; - patchedSource = `// Webpack Module ${String(id)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`; + patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(moduleId)}`; patchedFactory = (0, eval)(patchedSource); if (!patchedBy.has(patch.plugin)) { @@ -535,14 +583,14 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto markedAsPatched = true; } } catch (err) { - logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err); + logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\n`, err); if (IS_COMPANION_TEST) reporterData.failedPatches.erroredPatch.push({ ...patch, oldModule: lastCode, newModule: code, - id + id: moduleId }); if (IS_DEV) { @@ -558,7 +606,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto if (IS_COMPANION_TEST) reporterData.failedPatches.undoingPatchGroup.push({ ...patch, - id + id: moduleId }); code = previousCode; patchedFactory = previousFactory; @@ -575,7 +623,14 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto } } - return [patchedFactory, patchedSource, patchedBy]; + patchedFactory[SYM_ORIGINAL_FACTORY] = originalFactory; + + if (IS_DEV && patchedFactory !== originalFactory) { + originalFactory[SYM_PATCHED_SOURCE] = patchedSource; + originalFactory[SYM_PATCHED_BY] = patchedBy; + } + + return patchedFactory as PatchedModuleFactory; } function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) { diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 23ae913d..be9427b4 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -22,7 +22,8 @@ import { Logger } from "@utils/Logger"; import { canonicalizeMatch } from "@utils/patches"; import { traceFunction } from "../debug/Tracer"; -import { AnyModuleFactory, ModuleExports, WebpackRequire } from "./wreq"; +import { Flux } from "./common"; +import { AnyModuleFactory, AnyWebpackRequire, ModuleExports, WebpackRequire } from "./wreq"; const logger = new Logger("Webpack"); @@ -90,17 +91,13 @@ export const filters = { }; export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void; -export type FactoryListernFn = (factory: AnyModuleFactory) => void; +export type FactoryListernFn = (factory: AnyModuleFactory, moduleId: PropertyKey) => void; export const waitForSubscriptions = new Map(); export const moduleListeners = new Set(); export const factoryListeners = new Set(); export function _initWebpack(webpackRequire: WebpackRequire) { - if (webpackRequire.c == null) { - return; - } - wreq = webpackRequire; cache = webpackRequire.c; @@ -115,18 +112,38 @@ export function _initWebpack(webpackRequire: WebpackRequire) { // Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too 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; } @@ -134,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; @@ -411,7 +430,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; @@ -479,12 +501,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:", { @@ -492,7 +529,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]); @@ -500,8 +537,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 @@ -515,24 +553,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,"?([^)]+?)"?\)\)/; diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index dbc45105..ff28732c 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -33,27 +33,45 @@ export type AsyncModuleBody = ( asyncResult: (error?: any) => void ) => Promise; -export type ChunkHandlers = { +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: ChunkHandlers, chunkId: PropertyKey, promises: Promise) => void, + 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: ChunkHandlers, chunkId: PropertyKey, promises: Promise) => void, + 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 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) */ @@ -135,13 +153,20 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { * // exports is now { exportName: someExportedValue } (but each value is actually a getter) */ d: (this: WebpackRequire, exports: AnyRecord, definiton: AnyRecord) => void; - /** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */ - f: ChunkHandlers; + /** 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 */ @@ -162,18 +187,18 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { 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; + /** + * 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 @@ -185,6 +210,13 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { 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 @@ -200,12 +232,10 @@ export type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: M [SYM_PATCHED_BY]?: Set; }; -export type WrappedModuleFactory = AnyModuleFactory & { +export type PatchedModuleFactory = AnyModuleFactory & { [SYM_ORIGINAL_FACTORY]: AnyModuleFactory; [SYM_PATCHED_SOURCE]?: string; [SYM_PATCHED_BY]?: Set; }; -export type MaybeWrappedModuleFactory = AnyModuleFactory | WrappedModuleFactory; - -export type WrappedModuleFactories = Record; +export type MaybePatchedModuleFactory = PatchedModuleFactory | AnyModuleFactory; diff --git a/tsconfig.json b/tsconfig.json index feeaa8b4..c4c620c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,39 +21,18 @@ "jsx": "preserve", "baseUrl": "./src/", "paths": { - "@main/*": [ - "./main/*" - ], - "@api/*": [ - "./api/*" - ], - "@components/*": [ - "./components/*" - ], - "@utils/*": [ - "./utils/*" - ], - "@shared/*": [ - "./shared/*" - ], - "@webpack/types": [ - "./webpack/common/types" - ], - "@webpack/common": [ - "./webpack/common" - ], - "@webpack": [ - "./webpack/webpack" - ], - "@webpack/patch": [ - "./webpack/patchWebpack" - ], - "@plugins": [ - "./plugins" - ], - "@equicordplugins": [ - "./equicordplugins" - ] + "@main/*": ["./main/*"], + "@api/*": ["./api/*"], + "@components/*": ["./components/*"], + "@utils/*": ["./utils/*"], + "@shared/*": ["./shared/*"], + "@webpack/types": ["./webpack/common/types"], + "@webpack/common": ["./webpack/common"], + "@webpack": ["./webpack/webpack"], + "@webpack/patcher": ["./webpack/patchWebpack"], + "@webpack/wreq.d": ["./webpack/wreq.d"], + "@plugins/*": ["./plugins/*"], + "@equicordplugins/*": ["./equicordplugins/*"] }, "plugins": [ // Transform paths in output .d.ts files (Include this line if you output declarations files) @@ -70,4 +49,4 @@ "scripts/**/*", "eslint.config.mjs" ], -} \ No newline at end of file +}