chore: ChangeReporter

This commit is contained in:
ryan-0324 2024-07-06 20:22:28 -04:00
parent 366da53247
commit a9916d1b4c
37 changed files with 3673 additions and 598 deletions

View file

@ -61,6 +61,7 @@
"@typescript-eslint/non-nullable-type-assertion-style": "error", "@typescript-eslint/non-nullable-type-assertion-style": "error",
"@typescript-eslint/prefer-as-const": "error", "@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/require-await": "error", "@typescript-eslint/require-await": "error",
"@typescript-eslint/return-await": "error",
"quotes": ["error", "double", { "avoidEscape": true }], "quotes": ["error", "double", { "avoidEscape": true }],
"jsx-quotes": ["error", "prefer-double"], "jsx-quotes": ["error", "prefer-double"],
"no-mixed-spaces-and-tabs": "error", "no-mixed-spaces-and-tabs": "error",

66
.github/workflows/change-reporter.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: Change Reporter
on:
workflow_dispatch:
schedule:
# Every day at midnight
- cron: 0 0 * * *
jobs:
change-reporter:
if: github.repository == 'Vendicated/Vencord'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
if: ${{ github.event_name == 'schedule' }}
with:
ref: dev
- uses: actions/checkout@v4
if: ${{ github.event_name == 'workflow_dispatch' }}
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: ^20.11.0
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Google Chrome
id: setup-chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable
install-dependencies: true
- name: Build Vencord web standalone
run: pnpm buildWebStandalone --skip-extension
- name: Create Report (Stable)
timeout-minutes: 10
run: |
cd packages/discord-types
pnpm change-reporter
env:
CHANNEL: stable
CHROMIUM_BIN: ${{ steps.setup-chrome.outputs.chrome-path }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
VENCORD_DIST: ../../dist/browser.js
- name: Create Report (Canary)
timeout-minutes: 10
if: ${{ !cancelled() }} # run even if previous one failed
run: |
cd packages/discord-types
pnpm change-reporter
env:
CHANNEL: canary
CHROMIUM_BIN: ${{ steps.setup-chrome.outputs.chrome-path }}
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
VENCORD_DIST: ../../dist/browser.js

View file

@ -18,16 +18,17 @@ jobs:
- name: Use Node.js 20 - name: Use Node.js 20
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ^20.11.0 node-version: ^20.9.0
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Check packages/discord-types for TypeScript errors and lint - name: Check packages/discord-types for TypeScript errors and lint
run: | run: | # https://github.com/microsoft/TypeScript/issues/40431
tsc --emitDeclarationOnly
cd packages/discord-types cd packages/discord-types
pnpm test pnpm test
- name: Check if packages/discord-types is compatible with Vencord - name: Check if packages/discord-types is compatible with Vencord
run: pnpm testTsc run: pnpm testTsc && pnpm lint

View file

@ -34,47 +34,47 @@
"testTsc": "tsc --noEmit" "testTsc": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@sapphi-red/web-noise-suppressor": "0.3.3", "@sapphi-red/web-noise-suppressor": "0.3.5",
"@vap/core": "0.0.12", "@vap/core": "0.0.12",
"@vap/shiki": "0.10.5", "@vap/shiki": "0.10.5",
"eslint-plugin-simple-header": "^1.0.2", "eslint-plugin-simple-header": "^1.0.2",
"fflate": "^0.7.4", "fflate": "^0.8.2",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.50.0", "monaco-editor": "^0.50.0",
"nanoid": "^4.0.2", "nanoid": "^5.0.7",
"virtual-merge": "^1.0.1" "virtual-merge": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/chrome": "^0.0.246", "@types/chrome": "^0.0.268",
"@types/diff": "^5.2.1", "@types/diff": "^5.2.1",
"@types/html-minifier-terser": "^7.0.2", "@types/html-minifier-terser": "^7.0.2",
"@types/lodash": "~4.17.5", "@types/lodash": "~4.17.6",
"@types/node": "^18.19.39", "@types/node": "^18.19.39",
"@types/react": "~18.2.79", "@types/react": "~18.2.79",
"@types/react-dom": "~18.2.25", "@types/react-dom": "~18.2.25",
"@types/yazl": "^2.4.5", "@types/yazl": "^2.4.5",
"@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.14.1", "@typescript-eslint/parser": "^7.15.0",
"@vencord/discord-types": "workspace:^", "@vencord/discord-types": "workspace:^",
"diff": "^5.2.0", "diff": "^5.2.0",
"discord-types": "^1.3.3", "discord-types": "^1.3.3",
"esbuild": "^0.21.5", "esbuild": "^0.23.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "^2.1.0", "eslint-plugin-path-alias": "^2.1.0",
"eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^3.2.0", "eslint-plugin-unused-imports": "^3.2.0",
"highlight.js": "11.8.0", "highlight.js": "11.8.0",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"moment": "2.22.2", "moment": "2.22.2",
"puppeteer-core": "^19.11.1", "puppeteer-core": "^22.12.1",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"stylelint": "^15.11.0", "stylelint": "^15.11.0",
"stylelint-config-standard": "^33.0.0", "stylelint-config-standard": "^33.0.0",
"ts-patch": "^3.2.0", "ts-patch": "^3.2.1",
"tsx": "^4.15.7", "tsx": "^4.16.2",
"type-fest": "^4.20.1", "type-fest": "^4.21.0",
"typescript": "^5.5.2", "typescript": "^5.5.3",
"typescript-transform-paths": "^3.4.7", "typescript-transform-paths": "^3.4.7",
"zip-local": "^0.3.5" "zip-local": "^0.3.5"
}, },

View file

@ -1,9 +1,13 @@
import stylistic from "@stylistic/eslint-plugin"; import stylistic from "@stylistic/eslint-plugin";
// @ts-expect-error: No types
import checkFile from "eslint-plugin-check-file"; import checkFile from "eslint-plugin-check-file";
// @ts-expect-error: No types
import eslintPluginHeaders from "eslint-plugin-headers"; import eslintPluginHeaders from "eslint-plugin-headers";
import eslintPluginImport from "eslint-plugin-import-x"; import eslintPluginImport from "eslint-plugin-import-x";
import simpleImportSort from "eslint-plugin-simple-import-sort"; import simpleImportSort from "eslint-plugin-simple-import-sort";
// @ts-expect-error: No types
import eslintPluginUnicorn from "eslint-plugin-unicorn"; import eslintPluginUnicorn from "eslint-plugin-unicorn";
// @ts-expect-error: No types
import unusedImports from "eslint-plugin-unused-imports"; import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
@ -13,12 +17,12 @@ export default tseslint.config(
languageOptions: { languageOptions: {
parser: tseslint.parser, parser: tseslint.parser,
parserOptions: { parserOptions: {
project: ["./tsconfig.eslint.json", "./tsconfig.json"], projectService: true,
tsconfigRootDir: import.meta.dirname,
warnOnUnsupportedTypeScriptVersion: false warnOnUnsupportedTypeScriptVersion: false
} }
}, },
plugins: { plugins: {
// @ts-expect-error: https://github.com/eslint-stylistic/eslint-stylistic/issues/398#issuecomment-2178212946
"@stylistic": stylistic, "@stylistic": stylistic,
"@typescript-eslint": tseslint.plugin, "@typescript-eslint": tseslint.plugin,
"check-file": checkFile, "check-file": checkFile,
@ -34,7 +38,7 @@ export default tseslint.config(
"@stylistic/array-element-newline": ["error", "consistent"], "@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": ["error", "as-needed"], "@stylistic/arrow-parens": ["error", "as-needed"],
"@stylistic/block-spacing": "error", "@stylistic/block-spacing": "error",
"@stylistic/brace-style": "error", "@stylistic/brace-style": ["error", "1tbs", { allowSingleLine: true }],
"@stylistic/comma-dangle": ["error", "only-multiline"], "@stylistic/comma-dangle": ["error", "only-multiline"],
"@stylistic/comma-spacing": "error", "@stylistic/comma-spacing": "error",
"@stylistic/comma-style": "error", "@stylistic/comma-style": "error",
@ -60,7 +64,7 @@ export default tseslint.config(
"@stylistic/object-curly-spacing": ["error", "always"], "@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/rest-spread-spacing": "error", "@stylistic/rest-spread-spacing": "error",
"@stylistic/quote-props": ["error", "as-needed"], "@stylistic/quote-props": ["error", "as-needed"],
"@stylistic/quotes": "error", "@stylistic/quotes": ["error", "double", { avoidEscape: true }],
"@stylistic/semi": "error", "@stylistic/semi": "error",
"@stylistic/semi-spacing": "error", "@stylistic/semi-spacing": "error",
"@stylistic/semi-style": "error", "@stylistic/semi-style": "error",
@ -78,52 +82,55 @@ export default tseslint.config(
"@stylistic/type-named-tuple-spacing": "error", "@stylistic/type-named-tuple-spacing": "error",
"@typescript-eslint/adjacent-overload-signatures": "error", "@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error", "@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error", "@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/class-literal-property-style": "error", "@typescript-eslint/class-literal-property-style": "error",
"@typescript-eslint/consistent-generic-constructors": "error",
"@typescript-eslint/consistent-type-assertions": ["error", {
assertionStyle: "as",
objectLiteralTypeAssertions: "allow-as-parameter"
}],
"@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/consistent-type-exports": ["error", { fixMixedExportsWithInlineTypeSpecifier: true }], "@typescript-eslint/consistent-type-exports": ["error", { fixMixedExportsWithInlineTypeSpecifier: true }],
"@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }], "@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }],
"@typescript-eslint/member-ordering": ["error", {
default: {
memberTypes: [
"call-signature",
"signature",
"constructor",
["static-accessor", "static-field", "static-get", "static-method", "static-set"],
["accessor", "get", "method", "set"],
"field"
],
order: "alphabetically-case-insensitive"
}
}],
"@typescript-eslint/method-signature-style": "error", "@typescript-eslint/method-signature-style": "error",
"@typescript-eslint/naming-convention": ["error", { "@typescript-eslint/naming-convention": ["error", {
selector: "typeLike", selector: "typeLike",
format: ["PascalCase"] format: ["PascalCase"]
}], }],
"@typescript-eslint/no-confusing-void-expression": "error",
"@typescript-eslint/no-duplicate-enum-values": "error", "@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-duplicate-type-constituents": "error", "@typescript-eslint/no-duplicate-type-constituents": "error",
"@typescript-eslint/no-empty-object-type": "error", "@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-import-type-side-effects": "error", "@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/no-invalid-void-type": "error", "@typescript-eslint/no-invalid-void-type": "error",
"@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-redundant-type-constituents": "error", "@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error", "@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error", "@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-function-type": "error", "@typescript-eslint/no-unsafe-function-type": "error",
"@typescript-eslint/no-useless-empty-export": "error", "@typescript-eslint/no-useless-empty-export": "error",
"@typescript-eslint/no-wrapper-object-types": "error", "@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/non-nullable-type-assertion-style": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-function-type": "error", "@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/return-await": "error",
"@typescript-eslint/triple-slash-reference": "error", "@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unified-signatures": ["error", { ignoreDifferentlyNamedParameters: true }], "@typescript-eslint/unified-signatures": ["error", { ignoreDifferentlyNamedParameters: true }],
"check-file/filename-naming-convention": ["error", { "**/*.{mjs,ts}": "+([.0-9A-Za-z])" }], "check-file/filename-naming-convention": ["error", { "**/*.{mjs,ts}": "+([.0-9A-Za-z])" }],
"check-file/folder-naming-convention": ["error", { "**/": "CAMEL_CASE" }], "check-file/folder-naming-convention": ["error", { "**/": "CAMEL_CASE" }],
"import/extensions": "error",
"import/first": "error", "import/first": "error",
"import/newline-after-import": ["error", { considerComments: true }], // https://github.com/import-js/eslint-plugin-import/issues/2913
// "import/newline-after-import": ["error", { considerComments: true }],
"import/no-absolute-path": "error", "import/no-absolute-path": "error",
"import/no-duplicates": "error", "import/no-duplicates": "error",
"import/no-empty-named-blocks": "error", "import/no-empty-named-blocks": "error",
@ -138,7 +145,6 @@ export default tseslint.config(
"unicorn/no-hex-escape": "error", "unicorn/no-hex-escape": "error",
"unicorn/no-zero-fractions": "error", "unicorn/no-zero-fractions": "error",
"unicorn/number-literal-case": "error", "unicorn/number-literal-case": "error",
"unicorn/numeric-separators-style": ["error", { number: { minimumDigits: 0 } }],
"unicorn/prefer-export-from": ["error", { ignoreUsedVariables: true }], "unicorn/prefer-export-from": ["error", { ignoreUsedVariables: true }],
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": ["error", { "unused-imports/no-unused-vars": ["error", {
@ -150,11 +156,19 @@ export default tseslint.config(
} }
}, },
{ {
files: ["src/**/*"], files: ["!(src)/**/*", "*"],
rules: {
"simple-import-sort/imports": ["error", {
groups: [
["^((node:)?(assert(/strict)?|async_hooks|buffer|child_process|cluster|console|constants|crypto|dgram|diagnostics_channel|dns(/promises)?|domain|events|fs(/promises)?|http|http2|https|module|net|os|path(/(posix|win32))?|perf_hooks|process|punycode|querystring|readline(/promises)?|repl|stream(/(consumers|promises|web))?|string_decoder|timers(/promises)?|tls|trace_events|tty|url|util(/types)?|v8|vm|wasi|worker_threads|zlib)|node:test(/reporters)?)$"],
["^[^.]"]
]
}],
}
},
{
files: ["scripts/**/*", "src/**/*"],
rules: { rules: {
"@typescript-eslint/prefer-enum-initializers": "error",
// Disallow .d.ts files so that package consumers can use exported enums
"check-file/filename-blocklist": ["error", { "!**/!(*.d).ts": "!(*.d).ts" }],
"headers/header-format": ["error", { "headers/header-format": ["error", {
source: "string", source: "string",
content: [ content: [
@ -169,13 +183,36 @@ export default tseslint.config(
author: "Vencord project contributors" author: "Vencord project contributors"
} }
}], }],
"import/no-default-export": "error", }
},
{
files: ["src/**/*"],
rules: {
"@typescript-eslint/member-ordering": ["error", {
default: {
memberTypes: [
"call-signature",
"signature",
"constructor",
["static-accessor", "static-field", "static-get", "static-method", "static-set"],
["accessor", "get", "method", "set"],
"field"
],
order: "alphabetically-case-insensitive"
}
}],
"@typescript-eslint/prefer-enum-initializers": "error",
// Disallow .d.ts files so that package consumers can use exported enums
"check-file/filename-blocklist": ["error", { "!**/!(*.d).ts": "!(*.d).ts" }],
"import/extensions": "error",
// Does not work with ESLint 9
// "import/no-default-export": "error",
"import/no-extraneous-dependencies": ["error", { "import/no-extraneous-dependencies": ["error", {
devDependencies: false, devDependencies: false,
includeTypes: true includeTypes: true
}], }],
"import/no-unassigned-import": "error", "import/no-unassigned-import": "error",
"no-restricted-globals": ["error", "_", "IntlMessageFormat", "JSX", "React", "SimpleMarkdown"], "no-restricted-globals": ["error", "_", "IntlMessageFormat", "JSX", "NodeJS", "React", "SimpleMarkdown"],
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
`:expression:not(${[ `:expression:not(${[
@ -190,9 +227,16 @@ export default tseslint.config(
].join(", ")})`, ].join(", ")})`,
// Prefer naming function parameters instead of destructuring them // Prefer naming function parameters instead of destructuring them
":matches(ArrayPattern, ObjectPattern).params", ":matches(ArrayPattern, ObjectPattern).params",
// Prefer getters and setters
"[type=/^(TSAbstract)?AccessorProperty$/]",
// Disallow default exports
"ExportDefaultDeclaration",
// Disallow constructor definitions without parameters
"ClassDeclaration[superClass=null] MethodDefinition[kind=constructor][value.params.length=0]",
// Disallow enums that are const or ambient since package consumers cannot use them // Disallow enums that are const or ambient since package consumers cannot use them
"TSEnumDeclaration:matches([const=true], [declare=true])", "TSEnumDeclaration:matches([const=true], [declare=true])",
], ],
"unicorn/numeric-separators-style": ["error", { number: { minimumDigits: 0 } }],
} }
}, },
{ {

View file

@ -10,8 +10,9 @@
"directory": "packages/discord-types" "directory": "packages/discord-types"
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"files": ["src"], "files": ["src/**/!(tsconfig?(.*).json)"],
"scripts": { "scripts": {
"change-reporter": "tsx ./scripts/changeReporter/index.mts",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"test": "tsc --noEmit && eslint .", "test": "tsc --noEmit && eslint .",
@ -21,24 +22,29 @@
"dependencies": { "dependencies": {
"@types/events": "~3.0.3", "@types/events": "~3.0.3",
"@types/intl-messageformat": "~1.3.1", "@types/intl-messageformat": "~1.3.1",
"@types/lodash": "~4.17.5", "@types/lodash": "~4.17.6",
"@types/react": "~18.2.79", "@types/react": "~18.2.79",
"dependency-graph": "0.9.0", "dependency-graph": "0.9.0",
"moment": "2.22.2", "moment": "2.22.2",
"simple-markdown": "0.7.2", "simple-markdown": "0.7.2",
"type-fest": "^4.20.1" "type-fest": "^4.21.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint-types/unicorn": "^52.0.0",
"@stylistic/eslint-plugin": "^2.3.0", "@stylistic/eslint-plugin": "^2.3.0",
"eslint": "^9.5.0", "@types/node": "^20.14.10",
"@types/semver": "^7.5.8",
"@typescript-eslint/typescript-estree": "^8.0.0-alpha.39",
"eslint": "^9.6.0",
"eslint-plugin-check-file": "^2.8.0", "eslint-plugin-check-file": "^2.8.0",
"eslint-plugin-headers": "^1.1.2", "eslint-plugin-headers": "^1.1.2",
"eslint-plugin-import-x": "^0.5.2", "eslint-plugin-import-x": "^0.5.3",
"eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-unicorn": "^54.0.0",
"eslint-plugin-unused-imports": "^4.0.0", "eslint-plugin-unused-imports": "^4.0.0",
"typescript": "^5.5.2", "puppeteer-core": "^22.12.1",
"typescript-eslint": "^8.0.0-alpha.33" "semver": "^7.6.2",
"tsx": "^4.16.2",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0-alpha.39"
} }
} }

View file

@ -0,0 +1,714 @@
/*
* discord-types
* Copyright (C) 2024 Vencord project contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { join } from "path";
import type { CR } from "./types.mts";
export const config: CR.ReporterConfig = {
rootDir: join(import.meta.dirname, "../../src"),
deps: {
"../package.json": {
"@types/lodash": {
find() {
return this.Webpack.Common.lodash.VERSION;
},
overrides: [["4.17.x", "4.17.x"]],
},
"@types/react": {
find() {
return this.Webpack.Common.React.version;
},
overrides: [["18.2.x", "18.2.x"]],
},
moment: {
find() {
return this.Webpack.Common.moment.version;
},
},
},
},
src: {
"./flux/FluxActionHandlersGraph.ts": {
FluxActionHandlersGraph: {
type: "class",
},
},
"./flux/FluxActionLog.ts": {
FluxActionLog: {
type: "class",
find() {
return this.Webpack.Common.FluxDispatcher.actionLogger.log(
{ type: Math.random().toString() as any },
() => {}
).constructor;
},
},
},
"./flux/FluxActionLogger.ts": {
FluxActionLogger: {
type: "class",
},
},
"./flux/FluxDispatcher.ts": {
FluxDispatcher: {
type: "class",
},
FluxDispatchBand: {
type: "enum",
// Screaming snake case to pascal case (source enum's keys have no underscores)
keyMapper: key => key.replace(/(?<=^.).+/, s => s.toLowerCase()),
},
},
"./flux/FluxEmitter.ts": {
FluxEmitter: {
type: "class",
},
},
"./general/channels/ChannelRecord.ts": {
ChannelRecordBase: {
type: "class",
find() {
const { findByCode } = this.Webpack;
const constructor = findByCode("}isGroupDM(") ?? findByCode("{isGroupDM(");
return constructor && [Object.getPrototypeOf(constructor), constructor];
},
includeOptional: true,
ignoredRemovals: {
// Seems to have been removed
fields: ["voiceBackgroundDisplay"],
},
},
ForumLayout: {
type: "enum",
},
ThreadSortOrder: {
type: "enum",
},
ChannelFlags: {
type: "enum",
},
ThreadMemberFlags: {
type: "enum",
},
SafetyWarningType: {
type: "enum",
find() {
let key: string;
return new Promise<Record<string, unknown>>(res => {
this.Webpack.waitFor((exps: any) => {
for (const k in exps) {
try {
if (typeof exps[k]?.STRANGER_DANGER === "number") {
key = k;
return true;
}
} catch {}
}
return false;
}, exps => { res(exps[key]); });
});
},
},
ChannelType: {
type: "enum",
},
VideoQualityMode: {
type: "enum",
},
/*
// This seems to have been removed
VoiceCallBackgroundType: {
type: "enum",
},
*/
},
"./general/channels/ForumChannelRecord.ts": {
ForumChannelRecordBase: {
type: "class",
find() {
const castChannelRecord = this.Webpack.findByCode(".GUILD_TEXT]", "return(");
return castChannelRecord({ type: /* GUILD_FORUM */ 15 }).constructor;
},
ignoredRemovals: {
fields: true,
},
},
},
"./general/channels/GuildTextualChannelRecord.ts": {
GuildTextualChannelRecordBase: {
type: "class",
find() {
const castChannelRecord = this.Webpack.findByCode(".GUILD_TEXT]", "return(");
return Object.getPrototypeOf(castChannelRecord({ type: /* GUILD_TEXT */ 0 }).constructor);
},
ignoredRemovals: {
fields: true,
},
},
},
"./general/channels/GuildVocalChannelRecord.ts": {
GuildVocalChannelRecordBase: {
type: "class",
find() {
const castChannelRecord = this.Webpack.findByCode(".GUILD_TEXT]", "return(");
return Object.getPrototypeOf(castChannelRecord({ type: /* GUILD_VOICE */ 2 }).constructor);
},
ignoredRemovals: {
fields: true,
},
},
},
"./general/channels/PrivateChannelRecord.ts": {
PrivateChannelRecordBase: {
type: "class",
find() {
const castChannelRecord = this.Webpack.findByCode(".GUILD_TEXT]", "return(");
return Object.getPrototypeOf(castChannelRecord({ type: /* GROUP_DM */ 3 }).constructor);
},
ignoredAdditions: {
// Overrides
methods: ["isSystemDM"],
},
ignoredRemovals: {
fields: true,
},
},
},
"./general/channels/ThreadChannelRecord.ts": {
ThreadChannelRecord: {
type: "class",
find() {
const castChannelRecord = this.Webpack.findByCode(".GUILD_TEXT]", "return(");
return castChannelRecord({ type: /* PUBLIC_THREAD */ 11 }).constructor;
},
ignoredRemovals: {
fields: true,
},
},
},
"./general/channels/UnknownChannelRecord.ts": {
UnknownChannelRecord: {
type: "class",
find() {
return this.Webpack.findByCode(".UNKNOWN", "static fromServer(");
},
ignoredRemovals: {
fields: true,
},
},
},
"./general/emojis/Emoji.ts": {
UnicodeEmoji: {
type: "class",
},
EmojiType: {
type: "enum",
},
},
"./general/emojis/EmojiDisambiguations.ts": {
EmojiDisambiguations: {
type: "class",
find() {
return this.Webpack.Common.EmojiStore.getDisambiguatedEmojiContext().constructor;
},
},
},
"./general/emojis/GuildEmojis.ts": {
GuildEmojis: {
type: "class",
async find() {
const { Common } = this.Webpack;
let values = Object.values(Common.EmojiStore.getGuilds());
if (values.length <= 0)
await new Promise<void>(res => {
Common.EmojiStore.addConditionalChangeListener(() => {
values = Object.values(Common.EmojiStore.getGuilds());
if (values.length > 0) {
res();
return false;
}
});
Common.InstantInviteActionCreators.acceptInvite({
inviteKey: "discord-townhall"
}).catch(() => {});
});
return values[0]?.constructor;
},
},
},
"./general/i18n/FormattedMessage.ts": {
FormattedMessage: {
type: "class",
},
ASTNodeType: {
type: "enum",
// Undocumented
ignoredRemovals: [["HOOK"]],
},
},
"./general/i18n/Provider.ts": {
Provider: {
type: "class",
find() {
const { constructor } = this.Webpack.Common.i18n._provider;
return [Object.getPrototypeOf(constructor), constructor];
},
},
},
"./general/messages/ChannelMessages.ts": {
ChannelMessages: {
type: "class",
},
JumpType: {
type: "enum",
},
},
"./general/messages/InteractionRecord.ts": {
InteractionRecord: {
type: "class",
},
InteractionType: {
type: "enum",
// From the API documentation
ignoredRemovals: [["PING"]],
},
},
"./general/messages/MessageCache.ts": {
MessageCache: {
type: "class",
find() {
return this.Webpack.Common.MessageStore.getMessages("")._after.constructor;
},
},
},
"./general/messages/MessageRecord.ts": {
MessageRecord: {
type: "class",
},
ActivityActionType: {
type: "enum",
// From the API documentation
ignoredRemovals: [["SPECTATE"]],
},
CodedLinkType: {
type: "enum",
},
PollLayoutType: {
type: "enum",
},
PurchaseNotificationType: {
type: "enum",
},
ReactionType: {
type: "enum",
},
MessageState: {
type: "enum",
},
StickerFormat: {
type: "enum",
},
MetaStickerType: {
type: "enum"
},
},
"./general/messages/MessageSnapshotRecord.ts": {
MessageSnapshotRecord: {
type: "class",
},
},
"./general/messages/MinimalMessageRecord.ts": {
MinimalMessageRecord: {
type: "class",
},
MessageAttachmentFlags: {
type: "enum",
},
MessageButtonComponentStyle: {
type: "enum",
},
MessageSelectComponentOptionType: {
type: "enum",
},
MessageSelectComponentDefaultValueType: {
type: "enum",
find() {
let key: string;
return new Promise<Record<string, unknown>>(res => {
this.Webpack.waitFor((exps: any) => {
for (const k in exps) {
try {
if (exps[k]?.ROLE === "role") {
key = k;
return true;
}
} catch {}
}
return false;
}, exps => { res(exps[key]); });
});
},
},
MessageTextInputComponentStyle: {
type: "enum",
},
ContentScanFlags: {
type: "enum",
},
SeparatorSpacingSize: {
type: "enum",
},
MessageComponentType: {
type: "enum",
},
MessageEmbedFlags: {
type: "enum",
},
MessageEmbedType: {
type: "enum",
},
MessageFlags: {
type: "enum",
},
MessageType: {
type: "enum",
},
},
"./general/Activity.ts": {
ActivityFlags: {
type: "enum",
},
ActivityGamePlatform: {
type: "enum",
},
ActivityPlatform: {
type: "enum",
},
ActivityType: {
type: "enum",
},
},
"./general/ApplicationCommand.ts": {
InteractionContextType: {
type: "enum",
},
ApplicationCommandType: {
type: "enum",
},
ApplicationCommandOptionType: {
type: "enum",
},
},
"./general/ApplicationRecord.ts": {
ApplicationRecord: {
type: "class",
},
EmbeddedActivitySupportedPlatform: {
type: "enum",
},
EmbeddedActivityLabelType: {
type: "enum",
},
OrientationLockState: {
type: "enum",
},
ApplicationIntegrationType: {
type: "enum",
},
OAuth2Scope: {
type: "enum",
},
ApplicationFlags: {
type: "enum",
},
ApplicationOverlayMethodFlags: {
type: "enum",
},
ApplicationType: {
type: "enum",
},
},
"./general/Clan.ts": {
ClanBadgeKind: {
type: "enum",
},
ClanBannerKind: {
type: "enum",
},
ClanPlaystyle: {
type: "enum",
},
},
"./general/CompanyRecord.ts": {
CompanyRecord: {
type: "class",
},
},
/*
// This seems to have been removed
"./general/Draft.ts": {
DraftType: {
type: "enum",
},
},
*/
"./general/Frecency.ts": {
Frecency: {
type: "class",
},
},
"./general/GuildMember.ts": {
GuildMemberFlags: {
type: "enum",
},
},
"./general/GuildRecord.ts": {
GuildRecord: {
type: "class",
ignoredAdditions: {
// Overrides
methods: ["merge", "toString"],
},
},
UserNotificationSetting: {
type: "enum",
},
GuildExplicitContentFilterType: {
type: "enum",
},
GuildFeature: {
type: "enum",
},
/*
// Not exported; cannot be found
GuildHubType: {
type: "enum",
},
*/
MFALevel: {
type: "enum",
},
GuildNSFWContentLevel: {
type: "enum",
},
BoostedGuildTier: {
type: "enum",
},
SystemChannelFlags: {
type: "enum",
},
VerificationLevel: {
type: "enum",
},
},
"./general/ImmutableRecord.ts": {
ImmutableRecord: {
type: "class",
},
},
"./general/Permissions.ts": {
/*
// bigint enums are not yet possible: https://github.com/microsoft/TypeScript/issues/37783
Permissions: {
type: "enum",
},
*/
PermissionOverwriteType: {
type: "enum",
},
},
"./general/ReadState.ts": {
ReadState: {
type: "class",
async find() {
const { Common } = this.Webpack;
let me = Common.UserStore.getCurrentUser();
if (!me)
await new Promise<void>(res => {
Common.UserStore.addConditionalChangeListener(() => {
me = Common.UserStore.getCurrentUser();
if (me) {
res();
return false;
}
});
});
return Common.ReadStateStore.getNotifCenterReadState(me!.id)?.constructor;
},
},
ChannelNotificationSettingsFlags: {
type: "enum",
},
/*
// Not exported; cannot be found
ReadStateFlags: {
type: "enum",
},
*/
ReadStateType: {
type: "enum",
},
},
"./general/Role.ts": {
RoleFlags: {
type: "enum",
},
},
"./general/UserProfile.ts": {
PlatformType: {
type: "enum",
},
},
"./general/UserRecord.ts": {
UserRecord: {
type: "class",
ignoredAdditions: {
// Overrides
methods: ["toString"],
},
},
UserFlags: {
type: "enum",
// From the API documentation
ignoredRemovals: [["TEAM_PSEUDO_USER"]],
},
UserPremiumType: {
type: "enum",
},
},
"./stores/abstract/FluxPersistedStore.ts": {
FluxPersistedStore: {
type: "class",
ignoredAdditions: {
// Overrides
staticMethodsAndFields: ["destroy"],
methods: ["initializeIfNeeded"],
},
},
},
"./stores/abstract/FluxSnapshotStore.ts": {
FluxSnapshotStore: {
type: "class",
ignoredRemovals: {
// Exists on type to enforce that subclasses have `displayName`
staticMethodsAndFields: ["displayName"],
},
},
},
"./stores/abstract/FluxStore.ts": {
FluxStore: {
type: "class",
},
FluxChangeListeners: {
type: "class",
},
},
"./stores/ApplicationStore.ts": {
ApplicationStore: {
type: "class",
},
},
"./stores/ChannelStore.ts": {
ChannelStore: {
type: "class",
},
},
"./stores/DraftStore.ts": {
DraftStore: {
type: "class",
ignoredAdditions: {
// Overrides
staticMethodsAndFields: ["migrations"],
},
},
},
"./stores/EmojiStore.ts": {
EmojiStore: {
type: "class",
},
EmojiIntention: {
type: "enum",
},
},
"./stores/GuildChannelStore.ts": {
GuildChannelStore: {
type: "class",
},
},
"./stores/GuildMemberStore.ts": {
GuildMemberStore: {
type: "class",
},
},
"./stores/GuildStore.ts": {
GuildStore: {
type: "class",
},
},
"./stores/MessageStore.ts": {
MessageStore: {
type: "class",
},
},
"./stores/PermissionStore.ts": {
PermissionStore: {
type: "class",
},
},
"./stores/PresenceStore.ts": {
PresenceStore: {
type: "class",
},
ClientType: {
type: "enum",
// Undocumented
ignoredRemovals: [["EMBEDDED"]],
},
StatusType: {
type: "enum",
},
},
"./stores/ReadStateStore.ts": {
ReadStateStore: {
type: "class",
},
},
"./stores/RelationshipStore.ts": {
RelationshipStore: {
type: "class",
},
RelationshipType: {
type: "enum",
},
},
"./stores/SelectedChannelStore.ts": {
SelectedChannelStore: {
type: "class",
},
},
"./stores/SelectedGuildStore.ts": {
SelectedGuildStore: {
type: "class",
},
},
"./stores/UserProfileStore.ts": {
UserProfileStore: {
type: "class",
},
},
"./stores/UserStore.ts": {
UserStore: {
type: "class",
},
},
"./stores/WindowStore.ts": {
WindowStore: {
type: "class",
},
},
},
};

View file

@ -0,0 +1,735 @@
/*
* discord-types
* Copyright (C) 2024 Vencord project contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { readFile } from "fs/promises";
import { basename, join } from "path";
import { AST_NODE_TYPES, parse, type TSESTree } from "@typescript-eslint/typescript-estree";
import type { Page } from "puppeteer-core";
import { satisfies, subset, valid, validRange } from "semver";
import type { JsonObject, JsonValue } from "type-fest";
import { config } from "./config.mjs";
import type { CR } from "./types.mts";
import type { autoFindClass, autoFindEnum, autoFindStore } from "./utils.mts";
export async function getChangeReport(page: Page): Promise<CR.ChangeReport> {
const { rootDir, deps, src } = config;
const depsReports: Promise<CR.DependenciesReport>[] = [];
if (deps)
for (const filePath in deps)
depsReports.push(getDependenciesReport(page, join(rootDir, filePath), deps[filePath]!));
const srcReports: Promise<CR.SrcFileReport>[] = [];
if (src)
for (const filePath in src)
srcReports.push(getSrcFileReport(page, join(rootDir, filePath), src[filePath]!));
return {
deps: await Promise.all(depsReports),
src: await Promise.all(srcReports)
};
}
async function getDependenciesReport(page: Page, filePath: string, config: CR.DependenciesConfig) {
const fileName = basename(filePath);
const fileReport: CR.DependenciesReport = {
filePath,
fileName,
fileWarns: [],
passed: [],
warned: [],
failed: [],
errored: []
};
let dependencies: JsonValue | undefined;
try {
({ dependencies } = JSON.parse(await readFile(filePath, "utf-8")));
} catch (error) {
fileReport.fileError = `Failed to read and parse file '${fileName}':\n` + error;
return fileReport;
}
if (
typeof dependencies !== "object"
|| dependencies === null
|| Array.isArray(dependencies)
) {
fileReport.fileError = `File '${fileName}' does not have a valid dependencies object.`;
return fileReport;
}
for (const key in config) {
const dependencyConfig = config[key]!;
const report: CR.DependencyReport = {
name: key,
packageVersionRange: undefined,
discordVersion: undefined,
expectedVersionRange: undefined,
error: undefined,
warns: []
};
// https://github.com/microsoft/TypeScript/issues/53395
const packageVersionRange = (dependencies as JsonObject)[key];
if (typeof packageVersionRange !== "string") {
report.error = `File '${fileName}' does not have a dependency named '${key}'.`;
fileReport.errored.push(report);
continue;
}
report.packageVersionRange = packageVersionRange;
if (!validRange(packageVersionRange)) {
report.error = `Version range '${packageVersionRange}' for dependency '${key}' in file '${fileName}' is invalid.`;
fileReport.errored.push(report);
continue;
}
let discordVersion: unknown;
try {
discordVersion = await page.evaluate<[], CR.FindFunction<[]>>(
pageFunction(`return ${funcToString(dependencyConfig.find)}.call(Vencord);`)
);
} catch (error) {
report.error = `Find for version of dependency '${key}' errored:\n` + getErrorStack(error);
fileReport.errored.push(report);
continue;
}
if (typeof discordVersion !== "string") {
report.error = `Find for version of dependency '${key}' failed.`;
fileReport.errored.push(report);
continue;
}
report.discordVersion = discordVersion;
if (!valid(discordVersion)) {
report.error = `Find for version of dependency '${key}' returned an invalid SemVer version ('${discordVersion}').`;
fileReport.errored.push(report);
continue;
}
const { overrides } = dependencyConfig;
const expectedVersionRange = overrides?.find(([range]) => satisfies(discordVersion, range))?.[1]
?? discordVersion;
report.expectedVersionRange = expectedVersionRange;
if (subset(packageVersionRange, expectedVersionRange)) {
if (report.warns.length > 0)
fileReport.warned.push(report);
else
fileReport.passed.push(report);
} else
fileReport.failed.push(report);
}
return fileReport;
}
async function getSrcFileReport(page: Page, filePath: string, config: CR.SrcFileConfig) {
const fileName = basename(filePath);
const fileReport: CR.SrcFileReport = {
filePath,
fileName,
fileWarns: [],
unchanged: [],
warned: [],
changed: [],
errored: []
};
let ast: TSESTree.Program;
try {
ast = parse(await readFile(filePath, "utf-8"));
} catch (error) {
fileReport.fileError = `Failed to read and parse '${fileName}':\n` + error;
return fileReport;
}
const reports: Promise<CR.DeclarationReport>[] = [];
for (const node of ast.body) {
const declaration = node.type === AST_NODE_TYPES.ExportNamedDeclaration
? node.declaration
: node;
if (!declaration) continue;
switch (declaration.type) {
case AST_NODE_TYPES.ClassDeclaration: {
const { id } = declaration;
if (!id) continue;
const { name } = id;
const declarationConfig = config[name];
if (!declarationConfig) continue;
reports.push(getClassReport(
page,
// @ts-expect-error: Control flow narrowing bug
declaration,
declarationConfig
));
break;
}
case AST_NODE_TYPES.TSEnumDeclaration: {
const { name } = declaration.id;
const declarationConfig = config[name];
if (!declarationConfig) continue;
reports.push(getEnumReport(
page,
declaration,
declarationConfig
));
break;
}
}
}
for (const report of await Promise.all(reports)) {
if (report.error !== undefined) {
fileReport.errored.push(report);
} else {
const { changes } = report;
if (changes!.changedCount > 0)
fileReport.changed.push(report);
else if (report.warns.length > 0)
fileReport.warned.push(report);
else
fileReport.unchanged.push(report);
}
}
return fileReport;
}
/** Ensures config's type matches the type of the declaration found by the parser. */
function getSanitizedConfig<Report extends CR.DeclarationReport>(
config: CR.DeclarationConfig,
report: Report
): { class: CR.ClassConfig; enum: CR.EnumConfig; }[Report["type"]] {
const { type } = config;
const expectedType = report.type;
if (type === expectedType)
// @ts-expect-error: Bug
return config;
report.warns.push(`Expected config type for '${report.identifier}' to be '${expectedType}', but got '${type}'; config values will be ignored.`);
// @ts-expect-error: Bug
return { type: expectedType };
}
async function getClassReport(
page: Page,
declaration: TSESTree.ClassDeclarationWithName,
rawConfig: CR.DeclarationConfig
) {
const { name } = declaration.id;
const report: CR.ClassReport = {
type: "class",
identifier: name,
changes: undefined,
error: undefined,
warns: []
};
const config = getSanitizedConfig(rawConfig, report);
const source = getClassMembers(declaration.body.body, config, report);
const { find } = config;
if (find) {
try {
const changes = await page.evaluate<[CR.ClassMembers], CR.FindFunction<[CR.ClassMembers], CR.ClassChanges>>(
pageAsyncFunction("s", `const c = await ${funcToString(find)}.call(Vencord, s);`
+ "if (Array.isArray(c) ? c.length > 0 && c.every(isValidClass) : isValidClass(c))"
+ "return getClassChanges(c, s);"),
source
);
if (changes) {
checkClassIgnores(changes, config, report);
report.changes = changes;
return report;
}
report.warns.push(`Find for class '${name}' failed; attempting automatic class find.`);
} catch (error) {
report.warns.push(`Find for class '${name}' errored; attempting automatic class find:\n` + getErrorStack(error));
}
}
// Fast path for stores
if (/Store$/.test(name) && !declaration.abstract) {
try {
const changes = await page.evaluate<Parameters<typeof autoFindStore>, typeof autoFindStore>(
pageFunction("n", "s", "return autoFindStore(n, s);"),
name,
source
);
if (changes) {
checkClassIgnores(changes, config, report);
report.changes = changes;
return report;
}
report.warns.push(`Automatic store find for store '${name}' failed; attempting automatic class find.`);
} catch (error) {
report.warns.push(`Automatic store find for store '${name}' errored; attempting automatic class find:\n` + getErrorStack(error));
}
}
try {
const changes = await page.evaluate<Parameters<typeof autoFindClass>, typeof autoFindClass>(
pageFunction("s", "return autoFindClass(s);"),
source
);
if (changes) {
if (
// Ignore classes that do not have any members in common
// or classes that only have a constructor in common
changes.unchangedCount > 1
|| changes.unchangedCount > 0
&& (changes.additions.constructorDefinition
|| changes.removals.constructorDefinition)
) {
checkClassIgnores(changes, config, report);
report.changes = changes;
} else
report.error = `Automatic class find for class '${name}' failed. The target class may have too many additions.`;
} else
report.error = `Automatic class find for class '${name}' failed.`;
} catch (error) {
report.error = `Automatic class find for class '${name}' errored:\n` + getErrorStack(error);
}
return report;
}
function checkClassIgnores(
changes: CR.ClassChanges,
config: CR.ClassConfig,
report: CR.ClassReport
) {
const { additions, removals } = changes;
const { ignoredAdditions, ignoredRemovals } = config;
if (ignoredAdditions) {
const { constructorDefinition, ...rest } = ignoredAdditions;
if (constructorDefinition && removals.constructorDefinition)
report.warns.push(`Ignored addition 'constructorDefinition' in config for class '${report.identifier}' had no effect.`);
for (const key in rest) {
const memberCategory = key as keyof typeof rest;
const removedKeys = new Set(removals[memberCategory]);
for (const ignoredKey in rest[memberCategory])
if (removedKeys.has(ignoredKey))
report.warns.push(`Ignored addition '${ignoredKey}' of '${memberCategory}' in config for class '${report.identifier}' had no effect.`);
}
}
if (ignoredRemovals) {
const { constructorDefinition, ...rest } = ignoredRemovals;
if (constructorDefinition && additions.constructorDefinition)
report.warns.push(`Ignored removal 'constructorDefinition' in config for class '${report.identifier}' had no effect.`);
for (const key in rest) {
const memberCategory = key as keyof typeof rest;
const ignoredKeys = rest[memberCategory];
if (Array.isArray(ignoredKeys)) {
const addedKeys = new Set(additions[memberCategory]);
for (const ignoredKey in ignoredKeys)
if (addedKeys.has(ignoredKey))
report.warns.push(`Ignored removal '${ignoredKey}' of '${memberCategory}' in config for class '${report.identifier}' had no effect.`);
} else if (ignoredKeys && additions[memberCategory].length > 0)
report.warns.push(`Ignored removal for members of category '${memberCategory}' in config for class '${report.identifier}' had no effect.`);
}
}
}
function getClassMembers(
members: TSESTree.ClassElement[],
config: CR.ClassConfig,
report: CR.ClassReport
): CR.ClassMembers {
let constructorDefinition = false;
// Ignore duplicate members from overload signatures and config ignores
const staticMethodsAndFields = new Set<string>();
const staticGetters = new Set<string>();
const staticSetters = new Set<string>();
const methods = new Set<string>();
const getters = new Set<string>();
const setters = new Set<string>();
const fields = new Set<string>();
const { includeOptional } = config;
for (const [index, member] of members.entries()) {
switch (member.type) {
// Exclude abstract methods
case AST_NODE_TYPES.MethodDefinition: {
if (member.optional && !includeOptional) continue;
const name = getClassMemberName(member, index, report);
if (name === undefined) continue;
switch (member.kind) {
case "constructor":
// Ignore constructor definitions without parameters
if (member.value.params.length > 0)
constructorDefinition = true;
break;
case "method":
if (member.static)
staticMethodsAndFields.add(name);
else
methods.add(name);
break;
case "get":
if (member.static)
staticGetters.add(name);
else
getters.add(name);
break;
case "set":
if (member.static)
staticSetters.add(name);
else
setters.add(name);
break;
}
break;
}
// Exclude abstract properties
case AST_NODE_TYPES.PropertyDefinition: {
if (member.optional && !includeOptional) continue;
const name = getClassMemberName(member, index, report);
if (name === undefined) continue;
if (member.static)
staticMethodsAndFields.add(name);
else
fields.add(name);
break;
}
}
}
const { ignoredAdditions, ignoredRemovals } = config;
// Add ignored additions so as to not affect `changedCount`
if (ignoredAdditions) {
if (ignoredAdditions.constructorDefinition)
constructorDefinition = true;
if (ignoredAdditions.staticMethodsAndFields)
for (const key of ignoredAdditions.staticMethodsAndFields)
staticMethodsAndFields.add(key);
if (ignoredAdditions.staticGetters)
for (const key of ignoredAdditions.staticGetters)
staticGetters.add(key);
if (ignoredAdditions.staticSetters)
for (const key of ignoredAdditions.staticSetters)
staticSetters.add(key);
if (ignoredAdditions.methods)
for (const key of ignoredAdditions.methods)
methods.add(key);
if (ignoredAdditions.getters)
for (const key of ignoredAdditions.getters)
getters.add(key);
if (ignoredAdditions.setters)
for (const key of ignoredAdditions.setters)
setters.add(key);
if (ignoredAdditions.fields)
for (const key of ignoredAdditions.fields)
fields.add(key);
}
// Remove ignored removals so as to not affect `changedCount`
if (ignoredRemovals) {
if (ignoredRemovals.constructorDefinition)
constructorDefinition = false;
if (Array.isArray(ignoredRemovals.staticMethodsAndFields)) {
for (const key of ignoredRemovals.staticMethodsAndFields)
staticMethodsAndFields.delete(key);
} else if (ignoredRemovals.staticMethodsAndFields)
staticMethodsAndFields.clear();
if (Array.isArray(ignoredRemovals.staticGetters)) {
for (const key of ignoredRemovals.staticGetters)
staticGetters.delete(key);
} else if (ignoredRemovals.staticGetters)
staticGetters.clear();
if (Array.isArray(ignoredRemovals.staticSetters)) {
for (const key of ignoredRemovals.staticSetters)
staticSetters.delete(key);
} else if (ignoredRemovals.staticSetters)
staticSetters.clear();
if (Array.isArray(ignoredRemovals.methods)) {
for (const key of ignoredRemovals.methods)
methods.delete(key);
} else if (ignoredRemovals.methods)
methods.clear();
if (Array.isArray(ignoredRemovals.getters)) {
for (const key of ignoredRemovals.getters)
getters.delete(key);
} else if (ignoredRemovals.getters)
getters.clear();
if (Array.isArray(ignoredRemovals.setters)) {
for (const key of ignoredRemovals.setters)
setters.delete(key);
} else if (ignoredRemovals.setters)
setters.clear();
if (Array.isArray(ignoredRemovals.fields)) {
for (const key of ignoredRemovals.fields)
fields.delete(key);
} else if (ignoredRemovals.fields)
fields.clear();
}
return {
constructorDefinition: constructorDefinition,
staticMethodsAndFields: [...staticMethodsAndFields],
staticGetters: [...staticGetters],
staticSetters: [...staticSetters],
methods: [...methods],
getters: [...getters],
setters: [...setters],
fields: [...fields]
};
}
function getClassMemberName(
member: TSESTree.MethodDefinition | TSESTree.PropertyDefinition,
index: number,
report: CR.ClassReport
) {
const { key } = member;
switch (key.type) {
case AST_NODE_TYPES.Identifier: {
const { name } = key;
if (!member.computed)
return name;
report.warns.push(`Computed key '[${name}]' of member at index '${index}' of class '${report.identifier}' is unsupported; ignoring member.`);
return;
}
case AST_NODE_TYPES.Literal:
return String(key.value);
case AST_NODE_TYPES.MemberExpression: {
const { object, property } = key;
if (
object.type === AST_NODE_TYPES.Identifier
&& property.type === AST_NODE_TYPES.Identifier
) return `Symbol(${object.name}.${property.name})`;
break;
}
}
report.warns.push(`Computed key of member at index '${index}' of class '${report.identifier}' is unsupported; ignoring member.`);
}
async function getEnumReport(
page: Page,
declaration: TSESTree.TSEnumDeclaration,
rawConfig: CR.DeclarationConfig
) {
const { name } = declaration.id;
const report: CR.EnumReport = {
type: "enum",
identifier: name,
changes: undefined,
error: undefined,
warns: []
};
const config = getSanitizedConfig(rawConfig, report);
const source = getEnumMembers(declaration.body.members, config, report);
const { find } = config;
if (find) {
try {
const changes = await page.evaluate<[CR.EnumSource], CR.FindFunction<[CR.EnumSource], CR.EnumChanges>>(
pageAsyncFunction("s", `const o = await ${funcToString(find)}.call(Vencord, s);`
+ "if (isValidEnum(o)) return getEnumChanges(o);"),
source
);
if (changes) {
checkEnumIgnores(changes, config, report);
report.changes = changes;
return report;
}
report.warns.push(`Find for enum '${name}' failed; attempting automatic enum find.`);
} catch (error) {
report.warns.push(`Find for enum '${name}' errored; attempting automatic enum find:\n` + getErrorStack(error));
}
}
try {
const changes = await page.evaluate<Parameters<typeof autoFindEnum>, typeof autoFindEnum>(
pageFunction("o", "return autoFindEnum(o);"),
source
);
if (changes) {
// Ignore enums that do not have any members in common
if (changes.unchangedCount > 0) {
checkEnumIgnores(changes, config, report);
report.changes = changes;
} else
report.error = `Automatic enum find for enum '${name}' failed. The target enum may have too many additions.`;
} else
report.error = `Automatic enum find for enum '${name}' failed.`;
} catch (error) {
report.error = `Automatic enum find for enum '${name}' errored:\n` + getErrorStack(error);
}
return report;
}
function checkEnumIgnores(
changes: CR.EnumChanges,
config: CR.EnumConfig,
report: CR.EnumReport
) {
const { additions, removals } = changes;
const { ignoredAdditions, ignoredRemovals } = config;
if (ignoredAdditions)
for (const [key, value] of ignoredAdditions)
if (removals[key] === value)
report.warns.push(`Ignored addition '${key} = ${JSON.stringify(value)}' in config for enum '${report.identifier}' had no effect.`);
if (ignoredRemovals)
for (const [key, value] of ignoredRemovals)
if (value === undefined ? key in additions : additions[key] === value)
report.warns.push(`Ignored removal '${key}${value === undefined ? "" : " = " + JSON.stringify(value)}' in config for enum '${report.identifier}' had no effect.`);
return changes;
}
function getEnumMembers(
members: TSESTree.TSEnumMember[],
config: CR.EnumConfig,
report: CR.EnumReport
) {
const source: CR.EnumSource = {};
for (const [index, member] of members.entries()) {
if (member.computed) {
report.warns.push(`Key of member at index '${index}' of enum '${report.identifier}' is computed. Computed enum member keys are unsupported; ignoring member.`);
continue;
}
const { id } = member;
const key = id.type === AST_NODE_TYPES.Literal ? id.value : id.name;
const { initializer } = member;
if (!initializer) {
report.warns.push(`Member '${key}' of enum '${report.identifier}' has no initializer. Enum members without initializers are unsupported; ignoring member.`);
continue;
}
const { type } = initializer;
switch (type) {
case AST_NODE_TYPES.Literal: {
const { value } = initializer;
if (typeof value === "string" || typeof value === "number")
source[key] = value;
else
report.warns.push(`Literal initializer type '${typeof value}' of member '${key}' of enum '${report.identifier}' is unsupported; ignoring member.`);
break;
}
case AST_NODE_TYPES.BinaryExpression: {
const { operator } = initializer;
if (operator !== "<<") {
report.warns.push(`BinaryExpression initializer operator '${operator}' of member '${key}' of enum '${report.identifier}' is unsupported; ignoring member.`);
continue;
}
const { left, right } = initializer;
if (
left.type !== AST_NODE_TYPES.Literal
|| typeof left.value !== "number"
|| right.type !== AST_NODE_TYPES.Literal
|| typeof right.value !== "number"
) {
report.warns.push(`BinaryExpression initializer of member '${key}' of enum '${report.identifier}' is unsupported; ignoring member.`);
continue;
}
source[key] = left.value << right.value;
break;
}
default:
report.warns.push(`Initializer type '${type}' of member '${key}' of enum '${report.identifier}' is unsupported; ignoring member.`);
break;
}
}
const { keyMapper } = config;
if (keyMapper)
for (const key in source) {
const value = source[key]!;
delete source[key];
source[keyMapper(key)] = value;
}
const { ignoredAdditions, ignoredRemovals } = config;
// Add ignored additions so as to not affect similarity score
if (ignoredAdditions)
for (const [key, value] of ignoredAdditions)
source[key] = value;
// Remove ignored removals so as to not affect similarity score
if (ignoredRemovals)
for (const [key, value] of ignoredRemovals)
if (value === undefined || source[key] === value)
delete source[key];
return source;
}
function pageFunction(...args: [string, ...string[]]): any {
const body = args.pop()!;
return new Function(...args, `"use strict";${body}`);
}
const AsyncFunction: any = async function () {}.constructor;
function pageAsyncFunction(...args: [string, ...string[]]): any {
const body = args.pop()!;
return new AsyncFunction(...args, `"use strict";${body}`);
}
// Based on ECMA-262
const functionDeclarationOrArrowFunctionDefinitionRE = /^(?:async(?:[\t\v\f\uFEFF\p{Zs}]|\/\*.*\*\/)+)?(?:function[(*/\t\n\v\f\r\uFEFF\p{Zs}]|(?:\(|(?:[\p{IDS}$_]|\\u(?:[0-9A-Fa-f]{4}|\{[0-9A-Fa-f]{5}\}))(?:[\p{IDC}$]|\\u(?:[0-9A-Fa-f]{4}|\{[0-9A-Fa-f]{5}\}))+(?:[\t\n\v\f\r\uFEFF\p{Zs}]|\/\*.*\*\/)*=))/u;
/**
* Makes the result of `func.toString()` a callable expression when evaled.
* Does not support accessors, static methods, private methods/accessors,
* or method definitions with symbol keys.
*/
function funcToString(func: (...args: never[]) => unknown) {
const funcString = func.toString();
if (functionDeclarationOrArrowFunctionDefinitionRE.test(funcString))
return `(${funcString})`;
return `({${funcString}})[${JSON.stringify(func.name)}]`;
}
function getErrorStack(error: unknown) {
return typeof error === "object" && error !== null && "stack" in error
? error.stack
: error;
}

View file

@ -0,0 +1,74 @@
/*
* discord-types
* Copyright (C) 2024 Vencord project contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { readFileSync } from "fs";
import { join } from "path";
import puppeteer from "puppeteer-core";
import { validateEnv } from "../utils.mjs";
import { getChangeReport } from "./getChangeReport.mjs";
import { logSummary } from "./logSummary.mjs";
import { autoFindClass, autoFindEnum, autoFindStore, getClassChanges, getEnumChanges, isValidClass, isValidEnum } from "./utils.mjs";
validateEnv(process.env, {
CHANNEL: ["stable", "ptb", "canary"],
CHROMIUM_BIN: true,
DISCORD_TOKEN: true,
DISCORD_WEBHOOK: false,
VENCORD_DIST: true,
});
const { CHANNEL, CHROMIUM_BIN, DISCORD_TOKEN, DISCORD_WEBHOOK, VENCORD_DIST } = process.env;
const CWD = process.cwd();
const browser = await puppeteer.launch({
executablePath: CHROMIUM_BIN,
headless: true,
args: ["--no-sandbox"]
});
const page = await browser.newPage();
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
await page.setBypassCSP(true);
await page.evaluateOnNewDocument(`(() => {
"use strict";
if (/(?:^|\\.)discord\\.com$/.test(location.hostname)) {
${readFileSync(join(CWD, VENCORD_DIST), "utf-8")};
window.Vencord = Vencord;
window.CHANGE_REPORTER_LOGGED_IN = new Promise(res => {
Vencord.Webpack.waitFor("loginToken", async exps => {
await exps.loginToken(${JSON.stringify(DISCORD_TOKEN)});
res();
});
});
}
})();
`);
await page.goto(`https://${CHANNEL === "stable" ? "" : CHANNEL + "."}discord.com/login`);
await page.evaluate(`(async () => {
"use strict";
await CHANGE_REPORTER_LOGGED_IN;
window.autoFindStore = (${autoFindStore}).bind(Vencord);
window.autoFindClass = (${autoFindClass}).bind(Vencord);
window.isValidClass = ${isValidClass};
window.getClassChanges = ${getClassChanges};
window.autoFindEnum = (${autoFindEnum}).bind(Vencord);
window.isValidEnum = ${isValidEnum};
window.getEnumChanges = ${getEnumChanges};
})();
`);
const report = await getChangeReport(page);
browser.close();
logSummary(report, CHANNEL);
if (DISCORD_WEBHOOK) {
// TODO
}

View file

@ -0,0 +1,296 @@
/*
* discord-types
* Copyright (C) 2024 Vencord project contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { CR } from "./types.mts";
export function logSummary(report: CR.ChangeReport, channel: "stable" | "ptb" | "canary") {
const { deps, src } = report;
let summary = `# Change Report (${channel === "ptb" ? channel.toUpperCase() : capitalize(channel)})\n`;
let sections = "";
if (deps.length > 0) {
sections += `## ${deps.length} file${deps.length === 1 ? "" : "s"} with watched dependencies:\n`;
let fileToLogCount = 0;
let section = "";
for (const report of deps) {
const { fileName, fileError, fileWarns, passed, warned, failed, errored } = report;
const toLogCount = warned.length + failed.length + errored.length;
if (
toLogCount <= 0
&& fileError === undefined
&& fileWarns.length <= 0
) continue;
fileToLogCount++;
const count = passed.length + toLogCount;
section += `### ${count} watched dependenc${count === 1 ? "y" : "ies"} in \`${fileName}\`:\n`;
if (fileWarns.length > 0)
section += `\`${fileName}\` has ${fileWarns.length} file-level warning${fileWarns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(fileWarns);
if (fileError === undefined) {
section += passed.length + " passed without warnings. \n";
section += warned.length + " passed with warnings";
if (warned.length > 0) {
section += ": \n";
for (const { name, warns } of warned)
section += `* The report for \`${name}\` has ${warns.length} warning${warns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(warns);
} else
section += ". \n";
section += failed.length + " failed";
if (failed.length > 0) {
section += ": \n";
for (const { name, packageVersionRange, discordVersion, expectedVersionRange, warns } of failed) {
section += `* \`${name}\`: Expected range \`${expectedVersionRange}\` given range \`${discordVersion}\``
+ `, but got range \`${packageVersionRange}\`.\n`;
if (warns.length > 0)
section += `* The report for \`${name}\` has ${warns.length} warning${warns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(warns);
else
section += "\n";
}
} else
section += ". \n";
section += errored.length + " errored";
if (errored.length > 0) {
section += ": \n";
for (const { name, packageVersionRange, discordVersion, expectedVersionRange, error, warns } of errored) {
section += `* \`${name}\`: \`${fileName}\` version range: \`${packageVersionRange}\``
+ `, Found version: \`${discordVersion}\``
+ `, Expected version range: \`${expectedVersionRange}\`\n`;
if (warns.length > 0)
section += `* The report for \`${name}\` has ${warns.length} warning${warns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(warns);
section += `* The report for \`${name}\` has an error:\n` + formatError(error);
}
} else
section += ". \n";
} else
section += `\`${fileName}\` has a file-level error:\n` + formatError(fileError);
}
if (fileToLogCount > 0) {
const fileToNotLogCount = deps.length - fileToLogCount;
sections += `### ${fileToNotLogCount} file${fileToNotLogCount === 1 ? " has" : "s have"}`
+ " no watched dependencies that failed or have warnings.\n" + section;
} else
sections += "### All watched dependencies passed without warnings.\n";
}
if (src.length > 0) {
sections += `## ${src.length} file${src.length === 1 ? "" : "s"} with watched declarations:\n`;
let fileToLogCount = 0;
let section = "";
for (const report of src) {
const { fileName, fileError, fileWarns, unchanged, warned, changed, errored } = report;
const toLogCount = warned.length + changed.length + errored.length;
if (
toLogCount <= 0
&& fileError === undefined
&& fileWarns.length <= 0
) continue;
fileToLogCount++;
const count = unchanged.length + toLogCount;
section += `### ${count} watched declaration${count === 1 ? "" : "s"} in \`${fileName}\`:\n`;
if (fileWarns.length > 0)
section += `\`${fileName}\` has ${fileWarns.length} file-level warning${fileWarns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(fileWarns);
if (fileError === undefined) {
section += unchanged.length + ` ${unchanged.length === 1 ? "is" : "are"} unchanged without warnings. \n`;
section += warned.length + ` ${warned.length === 1 ? "is" : "are"} unchanged with warnings`;
if (warned.length > 0) {
section += ": \n";
for (const { type, identifier, warns } of warned)
section += `* The report for ${type} \`${identifier}\` has ${warns.length} warning${warns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(warns);
} else
section += ". \n";
section += changed.length + ` ha${changed.length === 1 ? "s" : "ve"} changes`;
if (changed.length > 0) {
section += ": \n";
for (const { type, identifier, changes, warns } of changed) {
let additionCount = 0;
let added = "";
let removalCount = 0;
let removed = "";
switch (type) {
case "class": {
const { additions, removals } = changes;
if (additions.constructorDefinition) {
additionCount++;
added += " * Constructor definition with parameters\n";
}
if (additions.staticMethodsAndFields.length > 0) {
additionCount += additions.staticMethodsAndFields.length;
added += " * Static methods and fields:\n" + formatKeyList(additions.staticMethodsAndFields, 3);
}
if (additions.staticGetters.length > 0) {
additionCount += additions.staticGetters.length;
added += " * Static getters:\n" + formatKeyList(additions.staticGetters, 3);
}
if (additions.staticSetters.length > 0) {
additionCount += additions.staticSetters.length;
added += " * Static setters:\n" + formatKeyList(additions.staticSetters, 3);
}
if (additions.methods.length > 0) {
additionCount += additions.methods.length;
added += " * Instance methods:\n" + formatKeyList(additions.methods, 3);
}
if (additions.getters.length > 0) {
additionCount += additions.getters.length;
added += " * Getters:\n" + formatKeyList(additions.getters, 3);
}
if (additions.setters.length > 0) {
additionCount += additions.setters.length;
added += " * Setters:\n" + formatKeyList(additions.setters, 3);
}
if (additions.fields.length > 0) {
additionCount += additions.fields.length;
added += " * Fields:\n" + formatKeyList(additions.fields, 3);
}
if (removals.constructorDefinition) {
removalCount++;
removed += " * Constructor definition with parameters\n";
}
if (removals.staticMethodsAndFields.length > 0) {
removalCount += removals.staticMethodsAndFields.length;
removed += " * Static methods and fields:\n" + formatKeyList(removals.staticMethodsAndFields, 3);
}
if (removals.staticGetters.length > 0) {
removalCount += removals.staticGetters.length;
removed += " * Static getters:\n" + formatKeyList(removals.staticGetters, 3);
}
if (removals.staticSetters.length > 0) {
removalCount += removals.staticSetters.length;
removed += " * Static setters:\n" + formatKeyList(removals.staticSetters, 3);
}
if (removals.methods.length > 0) {
removalCount += removals.methods.length;
removed += " * Instance methods:\n" + formatKeyList(removals.methods, 3);
}
if (removals.getters.length > 0) {
removalCount += removals.getters.length;
removed += " * Getters:\n" + formatKeyList(removals.getters, 3);
}
if (removals.setters.length > 0) {
removalCount += removals.setters.length;
removed += " * Setters:\n" + formatKeyList(removals.setters, 3);
}
if (removals.fields.length > 0) {
removalCount += removals.fields.length;
removed += " * Fields:\n" + formatKeyList(removals.fields, 3);
}
break;
}
case "enum": {
const { additions, removals } = changes;
const addedEntries = Object.entries(additions);
if (addedEntries.length > 0) {
additionCount = addedEntries.length;
added += formatEnumEntryList(addedEntries, 2);
}
const removedEntries = Object.entries(removals);
if (removedEntries.length > 0) {
removalCount = removedEntries.length;
removed += formatEnumEntryList(removedEntries, 2);
}
break;
}
}
const changeCount = additionCount + removalCount;
section += `* ${capitalize(type)} \`${identifier}\` has ${changeCount} change${changeCount === 1 ? "" : "s"}:\n`
+ ` * ${additionCount} addition${additionCount === 1 ? "" : "s"}`
+ (additionCount > 0 ? `:\n${added}` : ".\n")
+ ` * ${removalCount} removal${removalCount === 1 ? "" : "s"}`
+ (removalCount > 0 ? `:\n${removed}\n` : ".\n\n");
if (warns.length > 0)
section += `* The report for ${type} \`${identifier}\` has ${warns.length} warning${warns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(warns);
}
} else
section += ". \n";
section += errored.length + " errored";
if (errored.length > 0) {
section += ": \n";
for (const { type, identifier, warns, error } of errored) {
if (warns.length > 0)
section += `* The report for ${type} \`${identifier}\` has ${warns.length} warning${warns.length === 1 ? "" : "s"}:\n`
+ formatWarnList(warns);
section += `* The report for ${type} \`${identifier}\` has an error:\n${formatError(error)}`;
}
} else
section += ". \n";
} else
section += `\`${fileName}\` has a file-level error:\n${formatError(fileError)}`;
}
if (fileToLogCount > 0) {
const fileToNotLogCount = src.length - fileToLogCount;
sections += `### ${fileToNotLogCount} file${fileToNotLogCount === 1 ? " has" : "s have"}`
+ " no watched declarations with changes or warnings.\n" + section;
} else
sections += "### All watched declarations are unchanged without warnings.\n";
}
summary += sections || "## There are 0 watched dependencies and declarations.";
console.log(summary);
}
function capitalize(string: string) {
return string.replace(/^./, c => c.toUpperCase());
}
function formatWarnList(warns: string[]) {
return warns.map(formatError).join("");
}
function formatError(error: string) {
return `\`\`\`\n${error}\n\`\`\`\n`;
}
function formatKeyList(keys: string[], indentLevel = 0) {
const indent = " ".repeat(indentLevel);
return keys.map(key => indent + `* \`${key}\`\n`).join("");
}
function formatEnumEntryList(entries: [key: string, value: unknown][], indentLevel = 0) {
const indent = " ".repeat(indentLevel);
return entries.map(([key, value]) => indent + `* \`${key} = ${JSON.stringify(value)}\`\n`).join("");
}

View file

@ -0,0 +1,230 @@
/*
* discord-types
* Copyright (C) 2024 Vencord project contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// eslint-disable-next-line import/no-relative-packages
import type * as Vencord from "../../../../src/Vencord.ts";
export namespace CR {
export interface ChangeReport {
deps: DependenciesReport[];
src: SrcFileReport[];
}
export type FileReport = DependenciesReport | SrcFileReport;
interface FileReportBase {
filePath: string;
fileName: string;
/** Error that caused the report to be returned early. */
fileError?: string | undefined;
/** Contains warnings not specific to any dependency or declaration. */
fileWarns: string[];
}
export interface DependenciesReport extends FileReportBase {
/** Contains reports that passed with no warns and no error. */
passed: DependencyReport<false>[];
/** Contains reports that passed with warns and no error. */
warned: DependencyReport<false>[];
/** Contains reports that failed with no error and maybe warns. */
failed: DependencyReport<false>[];
/** Contains reports that have an error. */
errored: DependencyReport<true>[];
}
export interface DependencyReport<Errored extends boolean = boolean> {
/** The name of the dependency. */
name: string;
/** The version of the dependency in `package.json`. */
packageVersionRange: Errored extends true ? string | undefined : string;
/** The version of the dependency bundled with Discord. */
discordVersion: Errored extends true ? string | undefined : string;
/**
* The matched version range from the `overrides` property of {@link DependencyConfig}
* or, if none matched, the version of the dependency bundled with Discord.
*/
expectedVersionRange: Errored extends true ? string | undefined : string;
/** Error that caused the report to return early. */
error: Errored extends true ? string : undefined;
warns: string[];
}
export interface SrcFileReport extends FileReportBase {
/** Contains reports that have no changes, no warns, and no error. */
unchanged: DeclarationReport<false>[];
/** Contains reports that have warns, no changes, and no error. */
warned: DeclarationReport<false>[];
/** Contains reports that have changes, maybe warns, and no error. */
changed: DeclarationReport<false>[];
/** Contains reports that have an error. */
errored: DeclarationReport<true>[];
}
export type DeclarationReport<Errored extends boolean = boolean>
= ClassReport<Errored> | EnumReport<Errored>;
interface DeclarationReportBase<Errored extends boolean = boolean> {
type: string;
/** The declaration's identifier. */
identifier: string;
changes: Errored extends true ? undefined : object;
/** Error that caused the report to return early. */
error: Errored extends true ? string : undefined;
warns: string[];
}
export interface ClassReport<Errored extends boolean = boolean> extends DeclarationReportBase<Errored> {
type: "class";
identifier: string;
changes: Errored extends true ? undefined : ClassChanges;
}
export interface EnumReport<Errored extends boolean = boolean> extends DeclarationReportBase<Errored> {
type: "enum";
identifier: string;
changes: Errored extends true ? undefined : EnumChanges;
}
export interface ReporterConfig {
/** The directory file paths are relative to. */
rootDir: string;
deps?: { [filePath: string]: DependenciesConfig; } | undefined;
src?: { [filePath: string]: SrcFileConfig; } | undefined;
}
export interface DependenciesConfig {
[dependencyName: string]: DependencyConfig;
}
export interface DependencyConfig {
/**
* @returns The version of the dependency bundled with Discord or undefined if the version could not be found.
*/
find: FindFunction<[], string>;
/**
* Specifies the version range to expect in `package.json` given the version bundled with Discord.
*/
overrides?: [discordVersionRange: string, packageVersionRange: string][] | undefined;
}
export interface SrcFileConfig {
[identifier: string]: DeclarationConfig;
}
export type DeclarationConfig = ClassConfig | EnumConfig;
interface DeclarationConfigBase {
type: string;
find?: FindFunction | undefined;
}
export interface ClassConfig extends DeclarationConfigBase {
type: "class";
/**
* An automatic find will be performed if omitted.
* If multiple classes are returned, their members will be merged.
* @param source The source class.
* @returns The class or undefined if it could not be found.
*/
// https://github.com/microsoft/TypeScript/issues/20007
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
find?: FindFunction<[source: ClassMembers], Function[] | Function> | undefined;
/** Whether to include optional class members. */
includeOptional?: boolean | undefined;
/** Members expected to be added. */
ignoredAdditions?: Partial<ClassMembers> | undefined;
/**
* Members expected to be removed.
* If a category is `true`, all members in that category will be expected to be removed.
*/
ignoredRemovals?: {
[Key in keyof ClassMembers]?: ClassMembers[Key]
| (ClassMembers[Key] extends string[] ? boolean | undefined : never);
} | undefined;
}
export interface EnumConfig extends DeclarationConfigBase {
type: "enum";
/**
* An automatic find will be performed if omitted.
* @param source The source enum.
* @returns The enum or undefined if it could not be found.
*/
find?: FindFunction<[source: EnumSource], EnumMembers> | undefined;
/**
* Mapper function to modifiy the source enum's keys.
* Ignored additions and removals will not be modified by this function.
*/
keyMapper?: ((key: string) => string) | undefined;
/** Members expected to be added. */
ignoredAdditions?: [key: string, value: string | number][] | undefined;
/** Members expected to be removed. */
ignoredRemovals?: [key: string, value?: string | number][] | undefined;
}
export type FindFunction<Args extends unknown[] = any[], Return = unknown>
= (this: typeof Vencord, ...args: Args) =>
Promise<Return | undefined> | Return | undefined;
export type DeclarationChanges = ClassChanges | EnumChanges;
interface DeclarationChangesBase {
additions: object;
removals: object;
/** The number of added/removed members. */
unchangedCount: number;
/** The number of added/removed members. */
changedCount: number;
}
export interface ClassChanges extends DeclarationChangesBase {
additions: ClassMembers;
removals: ClassMembers;
}
export interface ClassMembers {
/** Whether the class has a constructor definition with parameters. */
constructorDefinition: boolean;
staticMethodsAndFields: string[];
staticGetters: string[];
staticSetters: string[];
methods: string[];
getters: string[];
setters: string[];
fields: string[];
}
export interface Class {
/** Constructor function */
new (...args: never[]): unknown;
/** Static members */
[key: PropertyKey]: unknown;
/** Instance methods and accessors */
readonly prototype: Record<PropertyKey, unknown>;
}
export interface EnumChanges extends DeclarationChangesBase {
additions: EnumMembers;
removals: EnumMembers;
}
export type EnumMembers = Record<string, unknown>;
export type EnumSource = Record<string, number> | Record<string, string>;
/** Excludes heterogeneous enums. */
export type Enum = NumericEnum | StringEnum;
/** Numeric enums have reverse mapping. */
export type NumericEnum
= { readonly [key: string]: number; }
& { readonly [value: number]: string; };
/** String enums do not have reverse mapping. */
export interface StringEnum {
readonly [key: string]: string;
}
}

View file

@ -0,0 +1,372 @@
/*
* discord-types
* Copyright (C) 2024 Vencord project contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// eslint-disable-next-line import/no-relative-packages
import type * as Vencord from "../../../../src/Vencord.ts";
import type { CR } from "./types.mts";
export function autoFindStore(this: typeof Vencord, name: string, source: CR.ClassMembers) {
const persistKeyRE = new RegExp(`^${name}(?:V\\d+)?$`);
const store: { constructor: CR.Class; } | undefined = this.Webpack.find((exp: any) => {
// Find stores from exported instances
const { constructor } = exp;
return typeof constructor === "function" && (
constructor.displayName === name
|| persistKeyRE.test(constructor.persistKey)
);
});
if (store)
return getClassChanges(store.constructor, source);
}
export function autoFindClass(this: typeof Vencord, source: CR.ClassMembers) {
let bestMatch: CR.ClassChanges | undefined;
let lowestChangedCount = Infinity;
const checked = new WeakSet<CR.Class>();
this.Webpack.find((exps: any) => {
for (const name in exps) {
let constructor: CR.Class;
// Some getters throw errors
try {
// Find classes from exported constructors
if (isValidClass(exps[name]))
constructor = exps[name];
// Find classes from exported instances
else if (isValidClass(exps[name]?.constructor))
({ constructor } = exps[name]);
else
continue;
} catch {
continue;
}
if (!checked.has(constructor)) {
checked.add(constructor);
const changes = getClassChanges(constructor, source);
const { changedCount } = changes;
if (changedCount < lowestChangedCount) {
lowestChangedCount = changedCount;
bestMatch = changes;
}
}
}
return false;
}, { isIndirect: true });
return bestMatch;
}
export function isValidClass(constructor: unknown): constructor is CR.Class {
if (typeof constructor !== "function")
return false;
const { prototype } = constructor;
return typeof prototype === "object" && prototype !== null;
}
export function getClassChanges(
constructors: [CR.Class, ...CR.Class[]] | CR.Class,
source: CR.ClassMembers
): CR.ClassChanges {
if (!Array.isArray(constructors))
constructors = [constructors];
let hasConstructorDefinition = false;
const constructorKeys = new Set<PropertyKey>();
const constructorDescriptors = new Map<PropertyKey, PropertyDescriptor>();
const prototypeKeys = new Set<PropertyKey>();
const prototypeDescriptors = new Map<PropertyKey, PropertyDescriptor>();
const matchedFields = new Set<string>();
// Ignore constructor definitions without parameters
const constructorRE = /[{}]constructor\([^)]/;
const fieldRE = /(?<=[{}]constructor\(.+?{.+\(this,")[^"]+(?=",)/g;
for (const constructor of constructors) {
const constructorString = constructor.toString();
if (constructorRE.test(constructorString))
hasConstructorDefinition = true;
const constDescriptors = Object.getOwnPropertyDescriptors(constructor);
for (const key of Object.getOwnPropertyNames(constructor)) {
constructorKeys.add(key);
constructorDescriptors.set(key, constDescriptors[key]!);
}
for (const key of Object.getOwnPropertySymbols(constructor)) {
constructorKeys.add(key);
constructorDescriptors.set(key, constDescriptors[key]!);
}
const { prototype } = constructor;
const protoDescriptors = Object.getOwnPropertyDescriptors(prototype);
for (const key of Object.getOwnPropertyNames(prototype)) {
prototypeKeys.add(key);
prototypeDescriptors.set(key, protoDescriptors[key]!);
}
for (const key of Object.getOwnPropertySymbols(prototype)) {
prototypeKeys.add(key);
prototypeDescriptors.set(key, protoDescriptors[key]!);
}
for (const [field] of constructorString.matchAll(fieldRE))
matchedFields.add(field);
}
const additions: CR.ClassMembers = {
constructorDefinition: false,
staticMethodsAndFields: [],
staticGetters: [],
staticSetters: [],
methods: [],
getters: [],
setters: [],
fields: []
};
let unchangedCount = 0;
let changedCount = 0;
// Constructor definition with parameters removal
let constructorDefinition = false;
if (hasConstructorDefinition) {
if (source.constructorDefinition) {
unchangedCount++;
} else {
additions.constructorDefinition = true;
changedCount++;
}
} else if (source.constructorDefinition) {
constructorDefinition = true;
changedCount++;
} else {
unchangedCount++;
}
// Static member removals
const staticMethodsAndFields = new Set(source.staticMethodsAndFields);
const staticGetters = new Set(source.staticGetters);
const staticSetters = new Set(source.staticSetters);
const ignoredConstructorKeys = new Set<PropertyKey>(["length", "name", "prototype"]);
for (const rawKey of constructorKeys) {
if (ignoredConstructorKeys.has(rawKey)) continue;
const descriptor = constructorDescriptors.get(rawKey)!;
const key = rawKey.toString();
if (descriptor.get) {
if (staticGetters.has(key)) {
staticGetters.delete(key);
unchangedCount++;
} else {
additions.staticGetters.push(key);
changedCount++;
}
if (descriptor.set) {
if (staticSetters.has(key)) {
staticSetters.delete(key);
unchangedCount++;
} else {
additions.staticSetters.push(key);
changedCount++;
}
}
continue;
}
if (descriptor.set) {
if (staticSetters.has(key)) {
staticSetters.delete(key);
unchangedCount++;
} else {
additions.staticSetters.push(key);
changedCount++;
}
continue;
}
if (staticMethodsAndFields.has(key)) {
staticMethodsAndFields.delete(key);
unchangedCount++;
} else {
additions.staticMethodsAndFields.push(key);
changedCount++;
}
}
changedCount += staticMethodsAndFields.size + staticGetters.size + staticSetters.size;
// Instance method and accessor removals
const methods = new Set(source.methods);
const getters = new Set(source.getters);
const setters = new Set(source.setters);
const ignoredPrototypeKeys = new Set<PropertyKey>(["constructor"]);
for (const rawKey of prototypeKeys) {
if (ignoredPrototypeKeys.has(rawKey)) continue;
const descriptor = prototypeDescriptors.get(rawKey)!;
const key = rawKey.toString();
if (descriptor.get) {
if (getters.has(key)) {
getters.delete(key);
unchangedCount++;
} else {
additions.getters.push(key);
changedCount++;
}
if (descriptor.set) {
if (setters.has(key)) {
setters.delete(key);
unchangedCount++;
} else {
additions.setters.push(key);
changedCount++;
}
}
continue;
}
if (descriptor.set) {
if (setters.has(key)) {
setters.delete(key);
unchangedCount++;
} else {
additions.setters.push(key);
changedCount++;
}
continue;
}
if (methods.has(key)) {
methods.delete(key);
unchangedCount++;
} else {
additions.methods.push(key);
changedCount++;
}
}
changedCount += methods.size + getters.size + setters.size;
// Field removals
const fields = new Set(source.fields);
for (const field of matchedFields) {
if (fields.has(field)) {
fields.delete(field);
unchangedCount++;
} else {
additions.fields.push(field);
changedCount++;
}
}
changedCount += fields.size;
return {
additions,
removals: {
constructorDefinition,
staticMethodsAndFields: [...staticMethodsAndFields],
staticGetters: [...staticGetters],
staticSetters: [...staticSetters],
methods: [...methods],
getters: [...getters],
setters: [...setters],
fields: [...fields]
},
unchangedCount,
changedCount
};
}
export function autoFindEnum(this: typeof Vencord, source: CR.EnumSource) {
let bestMatch: CR.EnumChanges | undefined;
let lowestChangedCount = Infinity;
const checked = new WeakSet();
this.Webpack.find((exps: any) => {
for (const name in exps) {
let exp: unknown;
// Some getters throw errors
try {
exp = exps[name];
} catch {
continue;
}
if (isValidEnum(exp) && !checked.has(exp)) {
checked.add(exp);
const changes = getEnumChanges(exp, source);
const { changedCount } = changes;
if (changedCount < lowestChangedCount) {
lowestChangedCount = changedCount;
bestMatch = changes;
}
}
}
return false;
}, { isIndirect: true });
return bestMatch;
}
export function isValidEnum(obj: unknown): obj is CR.EnumMembers {
return typeof obj === "object"
&& obj !== null
&& !Array.isArray(obj);
}
export function getEnumChanges(obj: CR.EnumMembers, source: CR.EnumSource): CR.EnumChanges {
const additions: CR.EnumMembers = {};
const removals: CR.EnumMembers = { ...source };
let unchangedCount = 0;
let changedCount = 0;
for (const key in obj) {
// Ignore numeric enum reverse mapping
if (parseFloat(key) === Number(key)) continue;
// Some getters throw errors
try {
const value = obj[key]!;
if (key in source && value === source[key]) {
delete removals[key];
unchangedCount++;
} else {
additions[key] = value;
changedCount++;
}
} catch {
changedCount = Infinity;
break;
}
}
changedCount += Object.keys(removals).length;
return {
additions,
removals,
unchangedCount,
changedCount
};
}

View file

@ -0,0 +1,45 @@
/*
* discord-types
* Copyright (C) 2024 Vencord project contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
type EnvConfig = Record<string, boolean | [string, ...string[]]>;
type ValidEnv<Config extends EnvConfig> = NodeJS.ProcessEnv & {
[Key in keyof Config as false extends Config[Key] ? never : Key]: Config[Key] extends string[]
? Config[Key][number]
: string;
} & { [Key in keyof Config as false extends Config[Key] ? Key : never]?: string; };
export function validateEnv<const Config extends EnvConfig>(
env: NodeJS.ProcessEnv,
config: Config
): asserts env is ValidEnv<Config> {
const errors: string[] = [];
for (const key in config) {
const varValue = env[key];
const varConfig = config[key]!;
if (varValue === undefined) {
if (varConfig)
errors.push(`TypeError: A value must be provided for required environment variable '${key}'.`);
} else if (Array.isArray(varConfig) && !varConfig.includes(varValue))
errors.push(`RangeError: The value provided for environment variable '${key}' must be one of ${formatChoices(varConfig)}.`);
}
if (errors.length > 0) {
for (const error of errors)
console.error(error);
process.exit(1);
}
}
function formatChoices(choices: string[]) {
const quotedChoices = choices.map(c => `'${c}'`);
if (choices.length < 3)
return quotedChoices.join(" or ");
const lastChoice = quotedChoices.pop()!;
return quotedChoices.join(", ") + ", or " + lastChoice;
}

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import type { FluxStore, FluxSyncWithFunction } from "../stores/abstract/FluxStore"; import type { FluxStore } from "../stores/abstract/FluxStore";
// Original name: Emitter // Original name: Emitter
export declare class FluxEmitter { export declare class FluxEmitter {
batched<T>(callback: () => T): T; batched<T>(callback: () => T): T;
destroy(): void; destroy(): void;
emit(): void; emit(): void;
emitNonReactOnce(syncWiths: Set<FluxSyncWithFunction>, changedStores: Set<FluxStore>): void; emitNonReactOnce(syncWiths: Set<() => unknown>, changedStores: Set<FluxStore>): void;
emitReactOnce(): void; emitReactOnce(): void;
getChangeSentinel(): number; getChangeSentinel(): number;
getIsPaused(): boolean; getIsPaused(): boolean;

View file

@ -18,7 +18,7 @@ export type FluxAction = FluxGenericAction<FluxActionType>;
export type ExcludeAction<Action extends FluxAction, ActionType extends Action["type"]> export type ExcludeAction<Action extends FluxAction, ActionType extends Action["type"]>
= Action extends unknown = Action extends unknown
// Workaround to avoid ts(2589) // Workaround to avoid ts(2589)
? [Exclude<Action["type"], ActionType>] extends [never] ? Exclude<Action["type"], ActionType> extends never
? never ? never
: { type: Exclude<Action["type"], ActionType>; } & Omit<Action, "type"> : { type: Exclude<Action["type"], ActionType>; } & Omit<Action, "type">
: never; : never;
@ -26,7 +26,7 @@ export type ExcludeAction<Action extends FluxAction, ActionType extends Action["
export type ExtractAction<Action extends FluxAction, ActionType extends Action["type"]> export type ExtractAction<Action extends FluxAction, ActionType extends Action["type"]>
= Action extends unknown = Action extends unknown
// Workaround to avoid ts(2589) // Workaround to avoid ts(2589)
? [Extract<Action["type"], ActionType>] extends [never] ? Extract<Action["type"], ActionType> extends never
? never ? never
: { type: Extract<Action["type"], ActionType>; } & Omit<Action, "type"> : { type: Extract<Action["type"], ActionType>; } & Omit<Action, "type">
: never; : never;

File diff suppressed because one or more lines are too long

View file

@ -34,7 +34,7 @@ export declare class InteractionRecord<
// Original name: InteractionTypes // Original name: InteractionTypes
export enum InteractionType { export enum InteractionType {
PING = 1, PING = 1, // From the API documentation
APPLICATION_COMMAND = 2, APPLICATION_COMMAND = 2,
MESSAGE_COMPONENT = 3, MESSAGE_COMPONENT = 3,
APPLICATION_COMMAND_AUTOCOMPLETE = 4, APPLICATION_COMMAND_AUTOCOMPLETE = 4,

View file

@ -5,7 +5,7 @@
*/ */
/** @internal */ /** @internal */
export type Bivariant<T extends (...args: any[]) => unknown> export type Bivariant<T extends (...args: never[]) => unknown>
// eslint-disable-next-line @typescript-eslint/method-signature-style // eslint-disable-next-line @typescript-eslint/method-signature-style
= { _(...args: Parameters<T>): ReturnType<T>; }["_"]; = { _(...args: Parameters<T>): ReturnType<T>; }["_"];

View file

@ -36,7 +36,11 @@ export declare class ReadStateStore<Action extends FluxAction = ReadStateStoreAc
getMentionChannelIds(): string[]; getMentionChannelIds(): string[];
getMentionCount(id: string, type?: ReadStateType | undefined /* = ReadStateType.CHANNEL */): number; getMentionCount(id: string, type?: ReadStateType | undefined /* = ReadStateType.CHANNEL */): number;
getNonChannelAckId(type: ReadStateType.NOTIFICATION_CENTER | ReadStateType.MESSAGE_REQUESTS): string | null; getNonChannelAckId(type: ReadStateType.NOTIFICATION_CENTER | ReadStateType.MESSAGE_REQUESTS): string | null;
getNotifCenterReadState(userId: string): ReadState<ReadStateType.NOTIFICATION_CENTER>; /**
* @param meId The user ID of the current user.
* @returns The ReadState object for the inbox of the current user. If the current user has not yet been loaded, undefined is returned.
*/
getNotifCenterReadState(meId: string): ReadState<ReadStateType.NOTIFICATION_CENTER> | undefined;
getOldestUnreadMessageId(id: string, type?: ReadStateType | undefined /* = ReadStateType.CHANNEL */): string | null; getOldestUnreadMessageId(id: string, type?: ReadStateType | undefined /* = ReadStateType.CHANNEL */): string | null;
getOldestUnreadTimestamp(id: string, type?: ReadStateType | undefined /* = ReadStateType.CHANNEL */): number; getOldestUnreadTimestamp(id: string, type?: ReadStateType | undefined /* = ReadStateType.CHANNEL */): number;
getReadStatesByChannel(): { [channelId: string]: ReadState<ReadStateType.CHANNEL>; }; getReadStatesByChannel(): { [channelId: string]: ReadState<ReadStateType.CHANNEL>; };

View file

@ -28,7 +28,7 @@ export declare class UserStore<
findByTag(username: string, discriminator?: string | Nullish): UserRecord | undefined; findByTag(username: string, discriminator?: string | Nullish): UserRecord | undefined;
forEach(callback: (user: UserRecord) => boolean | undefined): void; forEach(callback: (user: UserRecord) => boolean | undefined): void;
/** /**
* @returns The UserRecord object for the current user. If no USER_UPDATE action has been dispatched for the current user, undefined is returned. * @returns The UserRecord object for the current user. If the current user has not yet been loaded, undefined is returned.
*/ */
getCurrentUser(): UserRecord | undefined; getCurrentUser(): UserRecord | undefined;
getUser(userId?: string | Nullish): UserRecord | undefined; getUser(userId?: string | Nullish): UserRecord | undefined;

View file

@ -28,7 +28,10 @@ export declare abstract class FluxSnapshotStore<
static allStores: FluxSnapshotStore[]; static allStores: FluxSnapshotStore[];
static clearAll(): void; static clearAll(): void;
/** Not present on FluxSnapshotStore's constructor. */ /**
* Not present on FluxSnapshotStore's constructor.
* All subclasses are required to define their own.
*/
static displayName: string; static displayName: string;
clear(): void; clear(): void;

View file

@ -27,7 +27,7 @@ export declare abstract class FluxStore<Action extends FluxAction = FluxAction>
emitChange(): void; emitChange(): void;
getDispatchToken(): string; getDispatchToken(): string;
getName(): string; getName(): string;
initialize(...args: unknown[]): void; initialize(...args: never[]): void;
initializeIfNeeded(): void; initializeIfNeeded(): void;
mustEmitChanges( mustEmitChanges(
mustEmitChanges?: ((action: Action) => boolean) | Nullish /* = () => true */ mustEmitChanges?: ((action: Action) => boolean) | Nullish /* = () => true */
@ -38,7 +38,7 @@ export declare abstract class FluxStore<Action extends FluxAction = FluxAction>
): void; ): void;
syncWith( syncWith(
stores: FluxStore[], stores: FluxStore[],
func: FluxSyncWithFunction, func: () => unknown,
timeout?: number | Nullish timeout?: number | Nullish
): void; ): void;
waitFor(...stores: FluxStore[]): void; waitFor(...stores: FluxStore[]): void;
@ -51,31 +51,38 @@ export declare abstract class FluxStore<Action extends FluxAction = FluxAction>
_mustEmitChanges: Bivariant<((action: Action) => boolean)> | Nullish; _mustEmitChanges: Bivariant<((action: Action) => boolean)> | Nullish;
_reactChangeCallbacks: FluxChangeListeners; _reactChangeCallbacks: FluxChangeListeners;
_syncWiths: { _syncWiths: {
func: FluxSyncWithFunction; func: () => unknown;
store: FluxStore; store: FluxStore;
}[]; }[];
addChangeListener: FluxChangeListeners["add"]; addChangeListener: FluxChangeListeners["add"];
/**
* @param listener The change listener to add. It will be removed when it returns false.
*/
addConditionalChangeListener: FluxChangeListeners["addConditional"]; addConditionalChangeListener: FluxChangeListeners["addConditional"];
addReactChangeListener: FluxChangeListeners["add"]; addReactChangeListener: FluxChangeListeners["add"];
removeChangeListener: FluxChangeListeners["remove"]; removeChangeListener: FluxChangeListeners["remove"];
removeReactChangeListener: FluxChangeListeners["remove"]; removeReactChangeListener: FluxChangeListeners["remove"];
} }
export type FluxSyncWithFunction = () => boolean | undefined;
// Original name: ChangeListeners // Original name: ChangeListeners
export declare class FluxChangeListeners { export declare class FluxChangeListeners {
has(listener: FluxChangeListener): boolean; has(listener: FluxChangeListener): boolean;
hasAny(): boolean; hasAny(): boolean;
invokeAll(): void; invokeAll(): void;
add: (listener: FluxChangeListener) => void; add: (listener: FluxChangeListener<false>) => void;
/**
* @param listener The change listener to add. It will be removed when it returns false.
*/
addConditional: ( addConditional: (
listener: FluxChangeListener, listener: FluxChangeListener<true>,
immediatelyCall?: boolean | undefined /* = true */ immediatelyCall?: boolean | undefined /* = true */
) => void; ) => void;
listeners: Set<FluxChangeListener>; listeners: Set<FluxChangeListener>;
remove: (listener: FluxChangeListener) => void; remove: (listener: FluxChangeListener) => void;
} }
export type FluxChangeListener = () => boolean; export type FluxChangeListener<Conditional extends boolean = boolean>
= Conditional extends true
? () => unknown
: () => void;

View file

@ -0,0 +1,36 @@
{
"compilerOptions": {
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"strict": true,
"module": "Node16",
"typeRoots": ["../node_modules/@types"],
"types": [
"events",
"intl-messageformat",
"lodash",
"react"
],
"noEmit": true,
"isolatedModules": true,
"lib": [
"ES2019",
"ES2020.BigInt",
"ES2020.Date",
"ES2020.Intl",
"ES2020.Number",
"ES2020.Promise",
"ES2020.String",
"ES2021.String",
"ES2022.Array",
"ES2022.Object",
"ES2022.String",
"ES2023.Array"
],
"target": "ES2020"
}
}

View file

@ -1,19 +0,0 @@
{
"exclude": ["src"],
"compilerOptions": {
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"strict": true,
"module": "NodeNext",
"typeRoots": ["./node_modules/@eslint-types", "./node_modules/@types"],
"noEmit": true,
"allowJs": true,
"checkJs": true,
"target": "ESNext"
}
}

View file

@ -1,32 +1,27 @@
{ {
"include": ["src"], "exclude": ["src"],
"references": [{ "path": "../.." }],
"compilerOptions": { "compilerOptions": {
"exactOptionalPropertyTypes": true, "exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"strict": true, "strict": true,
"module": "Node16", "module": "NodeNext",
"types": ["events", "lodash", "react"], "typeRoots": ["./node_modules/@types"],
"types": ["node", "semver"],
"inlineSourceMap": true,
"noEmit": true, "noEmit": true,
"isolatedModules": true, "allowJs": true,
"checkJs": true,
"lib": [ "esModuleInterop": true,
"ES2019",
"ES2020.BigInt", "target": "ESNext",
"ES2020.Date",
"ES2020.Intl", // https://github.com/microsoft/TypeScript/issues/8836
"ES2020.Number", "skipLibCheck": true
"ES2020.Promise",
"ES2020.String",
"ES2021.String",
"ES2022.Array",
"ES2022.Object",
"ES2022.String",
"ES2023.Array"
],
"target": "ES2020"
} }
} }

View file

@ -16,18 +16,18 @@
"sideEffects": false, "sideEffects": false,
"types": "./index.d.ts", "types": "./index.d.ts",
"dependencies": { "dependencies": {
"@types/lodash": "~4.17.5", "@types/lodash": "~4.17.6",
"@types/node": "^18.19.39", "@types/node": "^18.19.39",
"@types/react": "~18.2.79", "@types/react": "~18.2.79",
"@types/react-dom": "~18.2.25", "@types/react-dom": "~18.2.25",
"@vencord/discord-types": "workspace:^", "@vencord/discord-types": "workspace:^",
"discord-types": "^1.3.3", "discord-types": "^1.3.3",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"type-fest": "^4.20.1" "type-fest": "^4.21.0"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"tsx": "^4.15.7" "tsx": "^4.16.2"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -36,7 +36,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
const CANARY = process.env.USE_CANARY === "true"; const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({ const browser = await pup.launch({
headless: "new", headless: true,
executablePath: process.env.CHROMIUM_BIN executablePath: process.env.CHROMIUM_BIN
}); });
@ -310,7 +310,7 @@ function reporterRuntime(token: string) {
} }
await page.evaluateOnNewDocument(` await page.evaluateOnNewDocument(`
if (location.host.endsWith("discord.com")) { if (/(?:^|\\.)discord\\.com$/.test(location.hostname)) {
${readFileSync("./dist/browser.js", "utf-8")}; ${readFileSync("./dist/browser.js", "utf-8")};
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
} }

View file

@ -11,12 +11,12 @@ import type { IpcRes } from "@utils/types";
import type { Settings } from "api/Settings"; import type { Settings } from "api/Settings";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
function invoke<T = any>(event: IpcEvents, ...args: unknown[]) { function invoke<T = any>(event: IpcEvents, ...args: unknown[]): Promise<T> {
return ipcRenderer.invoke(event, ...args) as Promise<T>; return ipcRenderer.invoke(event, ...args);
} }
export function sendSync<T = any>(event: IpcEvents, ...args: unknown[]) { export function sendSync<T = any>(event: IpcEvents, ...args: unknown[]): T {
return ipcRenderer.sendSync(event, ...args) as T; return ipcRenderer.sendSync(event, ...args);
} }
const PluginHelpers: Record<string, Record<string, (...args: any[]) => Promise<any>>> = {}; const PluginHelpers: Record<string, Record<string, (...args: any[]) => Promise<any>>> = {};

View file

@ -127,7 +127,7 @@ interface ContextMenuProps {
children: (ReactElement | null)[]; children: (ReactElement | null)[];
"aria-label": string; "aria-label": string;
onSelect: (() => void) | undefined; onSelect: (() => void) | undefined;
onClose: (callback: (...args: unknown[]) => unknown) => void; onClose: (callback: (...args: any[]) => unknown) => void;
} }
export function _usePatchContextMenu(props: ContextMenuProps) { export function _usePatchContextMenu(props: ContextMenuProps) {

View file

@ -50,7 +50,7 @@ const findCandidates = debounce(({ find, setError, setModule }: FindCandidatesOp
}); });
interface ReplacementComponentProps { interface ReplacementComponentProps {
module: [id: string | number, factory: (...args: unknown[]) => unknown]; module: [id: string | number, factory: (...args: any[]) => unknown];
match: string; match: string;
replacement: string | ReplaceFn; replacement: string | ReplaceFn;
setReplacementError: (error: any) => void; setReplacementError: (error: any) => void;

View file

@ -33,7 +33,7 @@ export interface UserContextProps {
className: string; className: string;
config: { context: string; }; config: { context: string; };
context: string; context: string;
onHeightUpdate: (...args: unknown[]) => void; onHeightUpdate: (...args: any[]) => void;
position: string; position: string;
target: HTMLElement; target: HTMLElement;
theme: string; theme: string;
@ -45,8 +45,8 @@ export interface StreamContextProps {
className: string; className: string;
config: { context: string; }; config: { context: string; };
context: string; context: string;
exitFullscreen: (...args: unknown[]) => void; exitFullscreen: (...args: any[]) => void;
onHeightUpdate: (...args: unknown[]) => void; onHeightUpdate: (...args: any[]) => void;
position: string; position: string;
stream: Stream; stream: Stream;
target: HTMLElement; target: HTMLElement;

View file

@ -101,7 +101,8 @@ function HiddenChannelLockScreen({ channel }: { channel: Exclude<GuildChannelRec
useEffect(() => { useEffect(() => {
const membersToFetch: string[] = []; const membersToFetch: string[] = [];
const guildOwnerId = GuildStore.getGuild(guildId)!.ownerId!; const guildOwnerId = GuildStore.getGuild(guildId)?.ownerId;
if (!guildOwnerId) return;
if (!GuildMemberStore.getMember(guildId!, guildOwnerId)) if (!GuildMemberStore.getMember(guildId!, guildOwnerId))
membersToFetch.push(guildOwnerId); membersToFetch.push(guildOwnerId);

View file

@ -162,7 +162,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
} }
} }
private req(method: "post" | "get" | "put", route: string, data: any = {}) { private req(method: "post" | "get" | "put", route: string, data: any = {}): Promise<any> {
if (this.device?.is_active) if (this.device?.is_active)
(data.query ??= {}).device_id = this.device.id; (data.query ??= {}).device_id = this.device.id;

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export function onlyOnce<F extends (...args: unknown[]) => unknown>(f: F): F { export function onlyOnce<F extends (...args: any[]) => unknown>(f: F): F {
let called = false; let called = false;
let result: any; let result: any;
return function onlyOnceWrapper(this: unknown) { return function onlyOnceWrapper(this: unknown) {
@ -25,6 +25,6 @@ export function onlyOnce<F extends (...args: unknown[]) => unknown>(f: F): F {
called = true; called = true;
// https://github.com/microsoft/TypeScript/issues/57164 // https://github.com/microsoft/TypeScript/issues/57164
return (result = f.apply(this, arguments as unknown as unknown[])); return (result = f.apply(this, arguments as unknown as Parameters<F>));
} as F; } as F;
} }

View file

@ -41,7 +41,7 @@ export interface Menu {
color?: string; color?: string;
render?: ComponentType<any>; render?: ComponentType<any>;
onChildrenScroll?: (...args: unknown[]) => unknown; onChildrenScroll?: (...args: any[]) => unknown;
childRowHeight?: number; childRowHeight?: number;
listClassName?: string; listClassName?: string;
disabled?: boolean; disabled?: boolean;

View file

@ -24,7 +24,8 @@
}, },
"resolveJsonModule": true, "resolveJsonModule": true,
"noEmit": true, "emitDeclarationOnly": true,
"outDir": "dist",
"plugins": [ "plugins": [
// Transform paths in output .d.ts files (Include this line if you output declarations files) // Transform paths in output .d.ts files (Include this line if you output declarations files)
@ -43,6 +44,8 @@
"DOM.Iterable", "DOM.Iterable",
"ESNext" "ESNext"
], ],
"target": "ESNext" "target": "ESNext",
"composite": true
} }
} }