diff --git a/.eslintrc.json b/.eslintrc.json index 809a8448bd25..fe9f20cddfe4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,9 +1,12 @@ { "root": true, - "extends": "marine/prettier/node", + "extends": ["neon/common", "neon/node", "neon/typescript", "neon/prettier"], "parserOptions": { "project": "./tsconfig.eslint.json" }, + "rules": { + "@typescript-eslint/consistent-type-definitions": ["error", "interface"] + }, "ignorePatterns": ["**/dist/*"], "env": { "jest": true diff --git a/packages/actions/__tests__/formatTag.test.ts b/packages/actions/__tests__/formatTag.test.ts index 1d4bc83c375d..47cc8ffd735b 100644 --- a/packages/actions/__tests__/formatTag.test.ts +++ b/packages/actions/__tests__/formatTag.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { formatTag } from '../src'; +import { formatTag } from '../src/index.js'; describe('Format Tag', () => { test('GIVEN tag with a prefix THEN format tag to not contain the prefix', () => { diff --git a/packages/actions/package.json b/packages/actions/package.json index e6656a59d7fc..bd507d9829b1 100644 --- a/packages/actions/package.json +++ b/packages/actions/package.json @@ -44,14 +44,9 @@ }, "devDependencies": { "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@vitest/coverage-c8": "^0.22.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/actions/src/formatTag/formatTag.ts b/packages/actions/src/formatTag/formatTag.ts index 4b8ce2560098..c1c792fe505d 100644 --- a/packages/actions/src/formatTag/formatTag.ts +++ b/packages/actions/src/formatTag/formatTag.ts @@ -1,4 +1,5 @@ export function formatTag(tag: string) { + // eslint-disable-next-line unicorn/no-unsafe-regex, prefer-named-capture-group const parsed = /(^@.*\/(?.*)@v?)?(?\d+.\d+.\d+)-?.*/.exec(tag); if (parsed?.groups) { diff --git a/packages/actions/src/formatTag/index.ts b/packages/actions/src/formatTag/index.ts index c078f1306905..031b89dc9b43 100644 --- a/packages/actions/src/formatTag/index.ts +++ b/packages/actions/src/formatTag/index.ts @@ -1,5 +1,5 @@ import { getInput, setOutput } from '@actions/core'; -import { formatTag } from './formatTag'; +import { formatTag } from './formatTag.js'; const tag = getInput('tag', { required: true }); const parsed = formatTag(tag); diff --git a/packages/actions/src/index.ts b/packages/actions/src/index.ts index 5119838f7058..4cfb594c252d 100644 --- a/packages/actions/src/index.ts +++ b/packages/actions/src/index.ts @@ -1 +1 @@ -export * from './formatTag/formatTag'; +export * from './formatTag/formatTag.js'; diff --git a/packages/api-extractor-utils/package.json b/packages/api-extractor-utils/package.json index ddb543cc0dc1..a13bb8924fb0 100644 --- a/packages/api-extractor-utils/package.json +++ b/packages/api-extractor-utils/package.json @@ -35,14 +35,8 @@ }, "devDependencies": { "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-tsdoc": "^0.2.16", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts b/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts index cd0923b850ee..8952b41162e8 100644 --- a/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts +++ b/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts @@ -18,12 +18,12 @@ import { type ApiConstructor, type ApiItemContainerMixin, } from '@microsoft/api-extractor-model'; -import { generateTypeParamData } from './TypeParameterJSONEncoder'; -import { type TokenDocumentation, resolveName, genReference, genToken, genParameter, generatePath } from './parse'; -import { createCommentNode } from './tsdoc'; +import { generateTypeParamData } from './TypeParameterJSONEncoder.js'; +import { type TokenDocumentation, resolveName, genReference, genToken, genParameter, generatePath } from './parse.js'; import type { DocBlockJSON } from './tsdoc/CommentBlock'; import type { AnyDocNodeJSON } from './tsdoc/CommentNode'; -import { type DocNodeContainerJSON, nodeContainer } from './tsdoc/CommentNodeContainer'; +import { type DocNodeContainerJSON, nodeContainer } from './tsdoc/CommentNodeContainer.js'; +import { createCommentNode } from './tsdoc/index.js'; export interface ReferenceData { name: string; @@ -31,9 +31,9 @@ export interface ReferenceData { } export interface InheritanceData { + parentKey: string; parentName: string; path: string; - parentKey: string; } export interface ApiInheritableJSON { @@ -41,23 +41,23 @@ export interface ApiInheritableJSON { } export interface ApiItemJSON { + comment: AnyDocNodeJSON | null; + containerKey: string; + deprecated: DocNodeContainerJSON | null; + excerpt: string; + excerptTokens: TokenDocumentation[]; kind: string; name: string; + path: string[]; referenceData: ReferenceData; - excerpt: string; - excerptTokens: TokenDocumentation[]; remarks: DocNodeContainerJSON | null; summary: DocNodeContainerJSON | null; - deprecated: DocNodeContainerJSON | null; - comment: AnyDocNodeJSON | null; - containerKey: string; - path: string[]; } export interface ApiPropertyItemJSON extends ApiItemJSON, ApiInheritableJSON { + optional: boolean; propertyTypeTokens: TokenDocumentation[]; readonly: boolean; - optional: boolean; } export interface ApiTypeParameterListJSON { @@ -65,11 +65,11 @@ export interface ApiTypeParameterListJSON { } export interface ApiTypeParameterJSON { - name: string; + commentBlock: DocBlockJSON | null; constraintTokens: TokenDocumentation[]; defaultTokens: TokenDocumentation[]; + name: string; optional: boolean; - commentBlock: DocBlockJSON | null; } export interface ApiParameterListJSON { @@ -81,29 +81,29 @@ export interface ApiMethodSignatureJSON ApiTypeParameterListJSON, ApiParameterListJSON, ApiInheritableJSON { - returnTypeTokens: TokenDocumentation[]; optional: boolean; overloadIndex: number; + returnTypeTokens: TokenDocumentation[]; } export interface ApiMethodJSON extends ApiMethodSignatureJSON { - static: boolean; protected: boolean; + static: boolean; } export interface ApiParameterJSON { - name: string; isOptional: boolean; - tokens: TokenDocumentation[]; + name: string; paramCommentBlock: DocBlockJSON | null; + tokens: TokenDocumentation[]; } export interface ApiClassJSON extends ApiItemJSON, ApiTypeParameterListJSON { constructor: ApiConstructorJSON | null; - properties: ApiPropertyItemJSON[]; - methods: ApiMethodJSON[]; extendsTokens: TokenDocumentation[]; implementsTokens: TokenDocumentation[][]; + methods: ApiMethodJSON[]; + properties: ApiPropertyItemJSON[]; } export interface ApiTypeAliasJSON extends ApiItemJSON, ApiTypeParameterListJSON { @@ -111,8 +111,8 @@ export interface ApiTypeAliasJSON extends ApiItemJSON, ApiTypeParameterListJSON } export interface EnumMemberData { - name: string; initializerTokens: TokenDocumentation[]; + name: string; summary: DocNodeContainerJSON | null; } @@ -121,19 +121,19 @@ export interface ApiEnumJSON extends ApiItemJSON { } export interface ApiInterfaceJSON extends ApiItemJSON, ApiTypeParameterListJSON { - properties: ApiPropertyItemJSON[]; - methods: ApiMethodSignatureJSON[]; extendsTokens: TokenDocumentation[][] | null; + methods: ApiMethodSignatureJSON[]; + properties: ApiPropertyItemJSON[]; } export interface ApiVariableJSON extends ApiItemJSON { - typeTokens: TokenDocumentation[]; readonly: boolean; + typeTokens: TokenDocumentation[]; } export interface ApiFunctionJSON extends ApiItemJSON, ApiTypeParameterListJSON, ApiParameterListJSON { - returnTypeTokens: TokenDocumentation[]; overloadIndex: number; + returnTypeTokens: TokenDocumentation[]; } export interface ApiConstructorJSON extends ApiItemJSON, ApiParameterListJSON { @@ -203,7 +203,7 @@ export class ApiNodeJSONEncoder { public static encodeParameterList( model: ApiModel, - item: ApiParameterListMixin & ApiDeclaredItem, + item: ApiDeclaredItem & ApiParameterListMixin, version: string, ): { parameters: ApiParameterJSON[] } { return { @@ -213,7 +213,7 @@ export class ApiNodeJSONEncoder { public static encodeTypeParameterList( model: ApiModel, - item: ApiTypeParameterListMixin & ApiDeclaredItem, + item: ApiDeclaredItem & ApiTypeParameterListMixin, version: string, ): ApiTypeParameterListJSON { return { diff --git a/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts b/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts index 43a028786936..d3c528df0679 100644 --- a/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts +++ b/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts @@ -1,13 +1,13 @@ import type { TypeParameter, ApiModel, ApiItem } from '@microsoft/api-extractor-model'; -import { type TokenDocumentation, genToken } from './parse'; -import { type DocBlockJSON, block } from './tsdoc/CommentBlock'; +import { type TokenDocumentation, genToken } from './parse.js'; +import { type DocBlockJSON, block } from './tsdoc/CommentBlock.js'; export interface TypeParameterData { - name: string; + commentBlock: DocBlockJSON | null; constraintTokens: TokenDocumentation[]; defaultTokens: TokenDocumentation[]; + name: string; optional: boolean; - commentBlock: DocBlockJSON | null; } export function generateTypeParamData( diff --git a/packages/api-extractor-utils/src/index.ts b/packages/api-extractor-utils/src/index.ts index 833d0a0f5e86..68ea66bd5251 100644 --- a/packages/api-extractor-utils/src/index.ts +++ b/packages/api-extractor-utils/src/index.ts @@ -1,4 +1,4 @@ -export * from './ApiNodeJSONEncoder'; -export * from './parse'; -export * from './tsdoc'; -export * from './TypeParameterJSONEncoder'; +export * from './ApiNodeJSONEncoder.js'; +export * from './parse.js'; +export * from './tsdoc/index.js'; +export * from './TypeParameterJSONEncoder.js'; diff --git a/packages/api-extractor-utils/src/parse.ts b/packages/api-extractor-utils/src/parse.ts index c1558843fc21..0938ce8eed42 100644 --- a/packages/api-extractor-utils/src/parse.ts +++ b/packages/api-extractor-utils/src/parse.ts @@ -14,8 +14,8 @@ import { } from '@microsoft/api-extractor-model'; import type { DocNode, DocParagraph, DocPlainText } from '@microsoft/tsdoc'; import { type Meaning, ModuleSource } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; -import { createCommentNode } from './tsdoc'; -import type { DocBlockJSON } from './tsdoc/CommentBlock'; +import type { DocBlockJSON } from './tsdoc/CommentBlock.js'; +import { createCommentNode } from './tsdoc/index.js'; export function findPackage(model: ApiModel, name: string): ApiPackage | undefined { return (model.findMembersByName(name)[0] ?? model.findMembersByName(`@discordjs/${name}`)[0]) as @@ -54,6 +54,7 @@ export function generatePath(items: readonly ApiItem[], version: string) { } } + // eslint-disable-next-line prefer-named-capture-group, unicorn/no-unsafe-regex return path.replace(/@discordjs\/(.*)\/(.*)?/, `$1/${version}/$2`); } @@ -70,26 +71,22 @@ export function resolveDocComment(item: ApiDocumentedItem) { const { summarySection } = tsdocComment; - function recurseNodes(nodes: readonly DocNode[] | undefined): string | null { - if (!nodes) { + function recurseNodes(node: DocNode | undefined): string | null { + if (!node) { return null; } - for (const node of nodes) { - switch (node.kind) { - case 'Paragraph': - return recurseNodes((node as DocParagraph).nodes); - case 'PlainText': - return (node as DocPlainText).text; - default: - return null; - } + switch (node.kind) { + case 'Paragraph': + return recurseNodes(node as DocParagraph); + case 'PlainText': + return (node as DocPlainText).text; + default: + return null; } - - return null; } - return recurseNodes(summarySection.nodes); + return recurseNodes(summarySection); } export function findReferences(model: ApiModel, excerpt: Excerpt) { @@ -107,6 +104,7 @@ export function findReferences(model: ApiModel, excerpt: Excerpt) { break; } + default: break; } @@ -142,16 +140,16 @@ export function getProperties(item: ApiItem) { } export interface TokenDocumentation { - text: string; - path: string | null; kind: string; + path: string | null; + text: string; } export interface ParameterDocumentation { - name: string; isOptional: boolean; - tokens: TokenDocumentation[]; + name: string; paramCommentBlock: DocBlockJSON | null; + tokens: TokenDocumentation[]; } function createDapiTypesURL(meaning: Meaning, name: string) { @@ -174,7 +172,7 @@ export function genReference(item: ApiItem, version: string) { export function genToken(model: ApiModel, token: ExcerptToken, version: string) { if (token.canonicalReference) { - // @ts-expect-error + // @ts-expect-error: Symbol is not publicly accessible token.canonicalReference._navigation = '.'; } diff --git a/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts b/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts index 5dea2609b254..2f7a039a5d0c 100644 --- a/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts +++ b/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts @@ -1,8 +1,8 @@ import type { ApiModel, ApiItem } from '@microsoft/api-extractor-model'; import type { DocBlock } from '@microsoft/tsdoc'; +import { blockTag, type DocBlockTagJSON } from './CommentBlockTag.js'; +import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode.js'; import { createCommentNode } from '.'; -import { blockTag, type DocBlockTagJSON } from './CommentBlockTag'; -import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode'; export interface DocBlockJSON extends DocNodeJSON { content: AnyDocNodeJSON[]; diff --git a/packages/api-extractor-utils/src/tsdoc/CommentBlockTag.ts b/packages/api-extractor-utils/src/tsdoc/CommentBlockTag.ts index 608188316b9f..214057c6137f 100644 --- a/packages/api-extractor-utils/src/tsdoc/CommentBlockTag.ts +++ b/packages/api-extractor-utils/src/tsdoc/CommentBlockTag.ts @@ -1,5 +1,5 @@ import type { DocBlockTag } from '@microsoft/tsdoc'; -import { type DocNodeJSON, node } from './CommentNode'; +import { type DocNodeJSON, node } from './CommentNode.js'; export interface DocBlockTagJSON extends DocNodeJSON { tagName: string; diff --git a/packages/api-extractor-utils/src/tsdoc/CommentCodeSpan.ts b/packages/api-extractor-utils/src/tsdoc/CommentCodeSpan.ts index 94b1766397a9..d8d42bf4fbf7 100644 --- a/packages/api-extractor-utils/src/tsdoc/CommentCodeSpan.ts +++ b/packages/api-extractor-utils/src/tsdoc/CommentCodeSpan.ts @@ -1,5 +1,5 @@ import type { DocCodeSpan } from '@microsoft/tsdoc'; -import { type DocNodeJSON, node } from './CommentNode'; +import { type DocNodeJSON, node } from './CommentNode.js'; export interface DocCodeSpanJSON extends DocNodeJSON { code: string; diff --git a/packages/api-extractor-utils/src/tsdoc/CommentNode.ts b/packages/api-extractor-utils/src/tsdoc/CommentNode.ts index 838676c7c285..5542a6bafa70 100644 --- a/packages/api-extractor-utils/src/tsdoc/CommentNode.ts +++ b/packages/api-extractor-utils/src/tsdoc/CommentNode.ts @@ -12,14 +12,14 @@ export interface DocNodeJSON { } export type AnyDocNodeJSON = - | DocNodeJSON - | DocPlainTextJSON - | DocNodeContainerJSON - | DocLinkTagJSON - | DocFencedCodeJSON | DocBlockJSON + | DocCodeSpanJSON | DocCommentJSON - | DocCodeSpanJSON; + | DocFencedCodeJSON + | DocLinkTagJSON + | DocNodeContainerJSON + | DocNodeJSON + | DocPlainTextJSON; export function node(node: DocNode): DocNodeJSON { return { diff --git a/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts b/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts index 115f529c228a..294d6843ed97 100644 --- a/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts +++ b/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts @@ -1,7 +1,7 @@ import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; import type { DocNodeContainer } from '@microsoft/tsdoc'; +import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode.js'; import { createCommentNode } from '.'; -import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode'; export interface DocNodeContainerJSON extends DocNodeJSON { nodes: AnyDocNodeJSON[]; diff --git a/packages/api-extractor-utils/src/tsdoc/FencedCodeCommentNode.ts b/packages/api-extractor-utils/src/tsdoc/FencedCodeCommentNode.ts index 254ece1905ba..52f7b26335f6 100644 --- a/packages/api-extractor-utils/src/tsdoc/FencedCodeCommentNode.ts +++ b/packages/api-extractor-utils/src/tsdoc/FencedCodeCommentNode.ts @@ -1,5 +1,5 @@ import type { DocFencedCode } from '@microsoft/tsdoc'; -import { type DocNodeJSON, node } from './CommentNode'; +import { type DocNodeJSON, node } from './CommentNode.js'; export interface DocFencedCodeJSON extends DocNodeJSON { code: string; diff --git a/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts b/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts index 3a0e14e00b5f..c1004bfb3c8e 100644 --- a/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts +++ b/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts @@ -1,17 +1,17 @@ import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; import type { DocDeclarationReference, DocLinkTag } from '@microsoft/tsdoc'; -import { type DocNodeJSON, node } from './CommentNode'; -import { resolveName, generatePath } from '../parse'; +import { resolveName, generatePath } from '../parse.js'; +import { type DocNodeJSON, node } from './CommentNode.js'; interface LinkTagCodeLink { - name: string; kind: string; + name: string; path: string; } export interface DocLinkTagJSON extends DocNodeJSON { - text: string | null; codeDestination: LinkTagCodeLink | null; + text: string | null; urlDestination: string | null; } diff --git a/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts b/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts index f564255c75e8..8e9010dea4a8 100644 --- a/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts +++ b/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts @@ -1,6 +1,6 @@ import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; import type { DocParamBlock } from '@microsoft/tsdoc'; -import { block, type DocBlockJSON } from './CommentBlock'; +import { block, type DocBlockJSON } from './CommentBlock.js'; interface DocParamBlockJSON extends DocBlockJSON { name: string; diff --git a/packages/api-extractor-utils/src/tsdoc/PlainTextCommentNode.ts b/packages/api-extractor-utils/src/tsdoc/PlainTextCommentNode.ts index 00774b1d11a1..097660a50c68 100644 --- a/packages/api-extractor-utils/src/tsdoc/PlainTextCommentNode.ts +++ b/packages/api-extractor-utils/src/tsdoc/PlainTextCommentNode.ts @@ -1,5 +1,5 @@ import type { DocPlainText } from '@microsoft/tsdoc'; -import { type DocNodeJSON, node } from './CommentNode'; +import { type DocNodeJSON, node } from './CommentNode.js'; export interface DocPlainTextJSON extends DocNodeJSON { text: string; diff --git a/packages/api-extractor-utils/src/tsdoc/RootComment.ts b/packages/api-extractor-utils/src/tsdoc/RootComment.ts index a8ee9a1f1677..19f10d85f985 100644 --- a/packages/api-extractor-utils/src/tsdoc/RootComment.ts +++ b/packages/api-extractor-utils/src/tsdoc/RootComment.ts @@ -1,14 +1,14 @@ import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; import type { DocComment } from '@microsoft/tsdoc'; +import { block, type DocBlockJSON } from './CommentBlock.js'; +import { type DocNodeJSON, node } from './CommentNode.js'; import { createCommentNode } from '.'; -import { block, type DocBlockJSON } from './CommentBlock'; -import { type DocNodeJSON, node } from './CommentNode'; export interface DocCommentJSON extends DocNodeJSON { - summary: DocNodeJSON[]; - remarks: DocNodeJSON[]; - deprecated: DocNodeJSON[]; customBlocks: DocBlockJSON[]; + deprecated: DocNodeJSON[]; + remarks: DocNodeJSON[]; + summary: DocNodeJSON[]; } export function comment(comment: DocComment, model: ApiModel, version: string, parentItem?: ApiItem): DocCommentJSON { diff --git a/packages/api-extractor-utils/src/tsdoc/index.ts b/packages/api-extractor-utils/src/tsdoc/index.ts index d8c12c2b48de..3784656fe22d 100644 --- a/packages/api-extractor-utils/src/tsdoc/index.ts +++ b/packages/api-extractor-utils/src/tsdoc/index.ts @@ -11,16 +11,15 @@ import { type DocCodeSpan, type DocParamBlock, } from '@microsoft/tsdoc'; -import { block } from './CommentBlock'; -import { codeSpan } from './CommentCodeSpan'; -import type { AnyDocNodeJSON } from './CommentNode'; -import { node as _node } from './CommentNode'; -import { nodeContainer } from './CommentNodeContainer'; -import { fencedCode } from './FencedCodeCommentNode'; -import { linkTagNode } from './LinkTagCommentNode'; -import { paramBlock } from './ParamBlock'; -import { plainTextNode } from './PlainTextCommentNode'; -import { comment } from './RootComment'; +import { block } from './CommentBlock.js'; +import { codeSpan } from './CommentCodeSpan.js'; +import { node as _node, type AnyDocNodeJSON } from './CommentNode.js'; +import { nodeContainer } from './CommentNodeContainer.js'; +import { fencedCode } from './FencedCodeCommentNode.js'; +import { linkTagNode } from './LinkTagCommentNode.js'; +import { paramBlock } from './ParamBlock.js'; +import { plainTextNode } from './PlainTextCommentNode.js'; +import { comment } from './RootComment.js'; export function createCommentNode( node: DocNode, @@ -51,13 +50,13 @@ export function createCommentNode( } } -export * from './CommentNode'; -export * from './CommentNodeContainer'; -export * from './CommentBlock'; -export * from './CommentBlockTag'; -export * from './CommentCodeSpan'; -export * from './FencedCodeCommentNode'; -export * from './LinkTagCommentNode'; -export * from './ParamBlock'; -export * from './PlainTextCommentNode'; -export * from './RootComment'; +export * from './CommentNode.js'; +export * from './CommentNodeContainer.js'; +export * from './CommentBlock.js'; +export * from './CommentBlockTag.js'; +export * from './CommentCodeSpan.js'; +export * from './FencedCodeCommentNode.js'; +export * from './LinkTagCommentNode.js'; +export * from './ParamBlock.js'; +export * from './PlainTextCommentNode.js'; +export * from './RootComment.js'; diff --git a/packages/builders/__tests__/components/actionRow.test.ts b/packages/builders/__tests__/components/actionRow.test.ts index 9bb74328e1ff..d0bb9f7b584f 100644 --- a/packages/builders/__tests__/components/actionRow.test.ts +++ b/packages/builders/__tests__/components/actionRow.test.ts @@ -1,4 +1,9 @@ -import { APIActionRowComponent, APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10'; +import { + ButtonStyle, + ComponentType, + type APIActionRowComponent, + type APIMessageActionRowComponent, +} from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { ActionRowBuilder, @@ -6,7 +11,7 @@ import { createComponentBuilder, SelectMenuBuilder, SelectMenuOptionBuilder, -} from '../../src'; +} from '../../src/index.js'; const rowWithButtonData: APIActionRowComponent = { type: ComponentType.ActionRow, diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts index 7bd5cadb23bb..11f34a3cb56f 100644 --- a/packages/builders/__tests__/components/button.test.ts +++ b/packages/builders/__tests__/components/button.test.ts @@ -1,12 +1,12 @@ import { - APIButtonComponentWithCustomId, - APIButtonComponentWithURL, ButtonStyle, ComponentType, + type APIButtonComponentWithCustomId, + type APIButtonComponentWithURL, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions'; -import { ButtonBuilder } from '../../src/components/button/Button'; +import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions.js'; +import { ButtonBuilder } from '../../src/components/button/Button.js'; const buttonComponent = () => new ButtonBuilder(); @@ -71,7 +71,7 @@ describe('Button Components', () => { }).toThrowError(); expect(() => { - // @ts-expect-error + // @ts-expect-error: invalid emoji const button = buttonComponent().setEmoji('test'); button.toJSON(); }).toThrowError(); @@ -103,9 +103,9 @@ describe('Button Components', () => { expect(() => buttonComponent().setStyle(24)).toThrowError(); expect(() => buttonComponent().setLabel(longStr)).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid parameter for disabled expect(() => buttonComponent().setDisabled(0)).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid emoji expect(() => buttonComponent().setEmoji('foo')).toThrowError(); expect(() => buttonComponent().setURL('foobar')).toThrowError(); diff --git a/packages/builders/__tests__/components/components.test.ts b/packages/builders/__tests__/components/components.test.ts index baa21ea66d77..520244d438eb 100644 --- a/packages/builders/__tests__/components/components.test.ts +++ b/packages/builders/__tests__/components/components.test.ts @@ -1,12 +1,12 @@ import { - APIActionRowComponent, - APIButtonComponent, - APIMessageActionRowComponent, - APISelectMenuComponent, - APITextInputComponent, ButtonStyle, ComponentType, TextInputStyle, + type APIButtonComponent, + type APIMessageActionRowComponent, + type APISelectMenuComponent, + type APITextInputComponent, + type APIActionRowComponent, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { @@ -15,7 +15,7 @@ import { createComponentBuilder, SelectMenuBuilder, TextInputBuilder, -} from '../../src/index'; +} from '../../src/index.js'; describe('createComponentBuilder', () => { test.each([ButtonBuilder, SelectMenuBuilder, TextInputBuilder])( @@ -67,7 +67,7 @@ describe('createComponentBuilder', () => { }); test('GIVEN an unknown component type THEN throws error', () => { - // @ts-expect-error + // @ts-expect-error: Unknown component type expect(() => createComponentBuilder({ type: 'invalid' })).toThrowError(); }); }); diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index e5b4e6e0300e..39a6c0e111ab 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -1,6 +1,6 @@ -import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v10'; +import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index'; +import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index.js'; const selectMenu = () => new SelectMenuBuilder(); const selectMenuOption = () => new SelectMenuOptionBuilder(); @@ -74,7 +74,8 @@ describe('Select Menu Components', () => { ]), ).not.toThrowError(); - const options = new Array(25).fill({ label: 'test', value: 'test' }); + const options = Array.from({ length: 25 }).fill({ label: 'test', value: 'test' }); + expect(() => selectMenu().addOptions(...options)).not.toThrowError(); expect(() => selectMenu().setOptions(...options)).not.toThrowError(); expect(() => selectMenu().addOptions(options)).not.toThrowError(); @@ -83,12 +84,13 @@ describe('Select Menu Components', () => { expect(() => selectMenu() .addOptions({ label: 'test', value: 'test' }) - .addOptions(...new Array(24).fill({ label: 'test', value: 'test' })), + + .addOptions(...Array.from({ length: 24 }).fill({ label: 'test', value: 'test' })), ).not.toThrowError(); expect(() => selectMenu() .addOptions([{ label: 'test', value: 'test' }]) - .addOptions(new Array(24).fill({ label: 'test', value: 'test' })), + .addOptions(Array.from({ length: 24 }).fill({ label: 'test', value: 'test' })), ).not.toThrowError(); }); @@ -96,33 +98,34 @@ describe('Select Menu Components', () => { expect(() => selectMenu().setCustomId(longStr)).toThrowError(); expect(() => selectMenu().setMaxValues(30)).toThrowError(); expect(() => selectMenu().setMinValues(-20)).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid disabled value expect(() => selectMenu().setDisabled(0)).toThrowError(); expect(() => selectMenu().setPlaceholder(longStr)).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions({ label: 'test' })).toThrowError(); expect(() => selectMenu().addOptions({ label: longStr, value: 'test' })).toThrowError(); expect(() => selectMenu().addOptions({ value: longStr, label: 'test' })).toThrowError(); expect(() => selectMenu().addOptions({ label: 'test', value: 'test', description: longStr })).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions({ label: 'test', value: 'test', default: 100 })).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions({ default: true })).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError(); expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError(); expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError(); expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', description: longStr }])).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', default: 100 }])).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions([{ value: 'test' }])).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid option expect(() => selectMenu().addOptions([{ default: true }])).toThrowError(); - const tooManyOptions = new Array(26).fill({ label: 'test', value: 'test' }); + const tooManyOptions = Array.from({ length: 26 }).fill({ label: 'test', value: 'test' }); + expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError(); expect(() => selectMenu().setOptions(tooManyOptions)).toThrowError(); @@ -141,9 +144,9 @@ describe('Select Menu Components', () => { selectMenuOption() .setLabel(longStr) .setValue(longStr) - // @ts-expect-error + // @ts-expect-error: invalid default value .setDefault(-1) - // @ts-expect-error + // @ts-expect-error: invalid emoji .setEmoji({ name: 1 }) .setDescription(longStr); }).toThrowError(); diff --git a/packages/builders/__tests__/components/textInput.test.ts b/packages/builders/__tests__/components/textInput.test.ts index 1e9b338f9834..62b0085f6505 100644 --- a/packages/builders/__tests__/components/textInput.test.ts +++ b/packages/builders/__tests__/components/textInput.test.ts @@ -1,4 +1,4 @@ -import { APITextInputComponent, ComponentType, TextInputStyle } from 'discord-api-types/v10'; +import { ComponentType, TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { labelValidator, @@ -7,10 +7,10 @@ import { placeholderValidator, valueValidator, textInputStyleValidator, -} from '../../src/components/textInput/Assertions'; -import { TextInputBuilder } from '../../src/components/textInput/TextInput'; +} from '../../src/components/textInput/Assertions.js'; +import { TextInputBuilder } from '../../src/components/textInput/TextInput.js'; -const superLongStr = 'a'.repeat(5000); +const superLongStr = 'a'.repeat(5_000); const textInputComponent = () => new TextInputBuilder(); @@ -47,7 +47,7 @@ describe('Text Input Components', () => { }); test('GIVEN invalid min length THEN validator does throw 2', () => { - expect(() => maxLengthValidator.parse(4001)).toThrowError(); + expect(() => maxLengthValidator.parse(4_001)).toThrowError(); }); test('GIVEN valid value THEN validator does not throw', () => { diff --git a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts index afa352ae1205..d2cbe3802788 100644 --- a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts +++ b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index'; +import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index.js'; const getBuilder = () => new ContextMenuCommandBuilder(); @@ -105,9 +105,9 @@ describe('Context Menu Commands', () => { }); test('GIVEN invalid name localizations THEN does throw error', () => { - // @ts-expect-error + // @ts-expect-error: invalid localization expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid localization expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError(); }); diff --git a/packages/builders/__tests__/interactions/SlashCommands/Options.test.ts b/packages/builders/__tests__/interactions/SlashCommands/Options.test.ts index 2c18fc696416..25875bb6716b 100644 --- a/packages/builders/__tests__/interactions/SlashCommands/Options.test.ts +++ b/packages/builders/__tests__/interactions/SlashCommands/Options.test.ts @@ -1,15 +1,15 @@ import { - APIApplicationCommandAttachmentOption, - APIApplicationCommandBooleanOption, - APIApplicationCommandChannelOption, - APIApplicationCommandIntegerOption, - APIApplicationCommandMentionableOption, - APIApplicationCommandNumberOption, - APIApplicationCommandRoleOption, - APIApplicationCommandStringOption, - APIApplicationCommandUserOption, ApplicationCommandOptionType, ChannelType, + type APIApplicationCommandAttachmentOption, + type APIApplicationCommandBooleanOption, + type APIApplicationCommandChannelOption, + type APIApplicationCommandIntegerOption, + type APIApplicationCommandMentionableOption, + type APIApplicationCommandNumberOption, + type APIApplicationCommandRoleOption, + type APIApplicationCommandStringOption, + type APIApplicationCommandUserOption, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { @@ -22,7 +22,7 @@ import { SlashCommandRoleOption, SlashCommandStringOption, SlashCommandUserOption, -} from '../../../src/index'; +} from '../../../src/index.js'; const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true); diff --git a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts index 642eb112919f..ef03954d5f34 100644 --- a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts +++ b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts @@ -1,4 +1,4 @@ -import { APIApplicationCommandOptionChoice, ChannelType, PermissionFlagsBits } from 'discord-api-types/v10'; +import { ChannelType, PermissionFlagsBits, type APIApplicationCommandOptionChoice } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { SlashCommandAssertions, @@ -14,7 +14,7 @@ import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder, SlashCommandUserOption, -} from '../../../src/index'; +} from '../../../src/index.js'; const largeArray = Array.from({ length: 26 }, () => 1 as unknown as APIApplicationCommandOptionChoice); @@ -33,9 +33,7 @@ const getSubcommandGroup = () => new SlashCommandSubcommandGroupBuilder().setNam const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123'); class Collection { - public get [Symbol.toStringTag]() { - return 'Map'; - } + public readonly [Symbol.toStringTag] = 'Map'; } describe('Slash Commands', () => { @@ -248,16 +246,16 @@ describe('Slash Commands', () => { }); test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => { - // @ts-expect-error + // @ts-expect-error: invalid max value expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue('test'))).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid max value expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue('test'))).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid min value expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue('test'))).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid min value expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue('test'))).toThrowError(); expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1.5))).toThrowError(); @@ -444,9 +442,9 @@ describe('Slash Commands', () => { }); test('GIVEN invalid name localizations THEN does throw error', () => { - // @ts-expect-error + // @ts-expect-error: invalid localization expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid localization expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError(); }); @@ -467,9 +465,9 @@ describe('Slash Commands', () => { }); test('GIVEN invalid description localizations THEN does throw error', () => { - // @ts-expect-error + // @ts-expect-error: invalid localization description expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid localization description expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError(); }); diff --git a/packages/builders/__tests__/interactions/modal.test.ts b/packages/builders/__tests__/interactions/modal.test.ts index 63a6adb126b0..3eaa934ad074 100644 --- a/packages/builders/__tests__/interactions/modal.test.ts +++ b/packages/builders/__tests__/interactions/modal.test.ts @@ -1,22 +1,22 @@ import { - APIModalInteractionResponseCallbackData, - APITextInputComponent, ComponentType, TextInputStyle, + type APIModalInteractionResponseCallbackData, + type APITextInputComponent, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { ActionRowBuilder, ButtonBuilder, ModalBuilder, - ModalActionRowComponentBuilder, TextInputBuilder, -} from '../../src'; + type ModalActionRowComponentBuilder, +} from '../../src/index.js'; import { componentsValidator, titleValidator, validateRequiredParameters, -} from '../../src/interactions/modals/Assertions'; +} from '../../src/interactions/modals/Assertions.js'; const modal = () => new ModalBuilder(); @@ -46,7 +46,7 @@ describe('Modals', () => { test('GIVEN invalid required parameters THEN validator does throw', () => { expect(() => - // @ts-expect-error + // @ts-expect-error: missing required parameter validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]), ).toThrowError(); }); @@ -66,7 +66,7 @@ describe('Modals', () => { test('GIVEN invalid fields THEN builder does throw', () => { expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError(); - // @ts-expect-error + // @ts-expect-error: customId is invalid expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError(); }); diff --git a/packages/builders/__tests__/messages/embed.test.ts b/packages/builders/__tests__/messages/embed.test.ts index f7d234083419..0c33255bf0c9 100644 --- a/packages/builders/__tests__/messages/embed.test.ts +++ b/packages/builders/__tests__/messages/embed.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { EmbedBuilder, embedLength } from '../../src'; +import { EmbedBuilder, embedLength } from '../../src/index.js'; const alpha = 'abcdefghijklmnopqrstuvwxyz'; @@ -74,7 +74,7 @@ describe('Embed', () => { test('GIVEN an embed with an invalid description THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setDescription('a'.repeat(4097))).toThrowError(); + expect(() => embed.setDescription('a'.repeat(4_097))).toThrowError(); }); }); @@ -130,11 +130,11 @@ describe('Embed', () => { test('GIVEN an embed with an invalid color THEN throws error', () => { const embed = new EmbedBuilder(); - // @ts-expect-error + // @ts-expect-error: invalid color expect(() => embed.setColor('RED')).toThrowError(); - // @ts-expect-error + // @ts-expect-error: invalid color expect(() => embed.setColor([42, 36])).toThrowError(); - expect(() => embed.setColor([42, 36, 1000])).toThrowError(); + expect(() => embed.setColor([42, 36, 1_000])).toThrowError(); }); }); @@ -307,7 +307,7 @@ describe('Embed', () => { test('GIVEN an embed with invalid footer text THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setFooter({ text: 'a'.repeat(2049) })).toThrowError(); + expect(() => embed.setFooter({ text: 'a'.repeat(2_049) })).toThrowError(); }); }); @@ -411,7 +411,7 @@ describe('Embed', () => { test('4', () => { const embed = new EmbedBuilder(); - expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError(); + expect(() => embed.addFields({ name: '', value: 'a'.repeat(1_025) })).toThrowError(); }); }); }); diff --git a/packages/builders/__tests__/messages/formatters.test.ts b/packages/builders/__tests__/messages/formatters.test.ts index 92a291f81ac7..467cf547de53 100644 --- a/packages/builders/__tests__/messages/formatters.test.ts +++ b/packages/builders/__tests__/messages/formatters.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-template-curly-in-string */ import { URL } from 'node:url'; import { describe, test, expect, vitest } from 'vitest'; import { @@ -21,7 +22,7 @@ import { TimestampStyles, underscore, userMention, -} from '../../src'; +} from '../../src/index.js'; describe('Message formatters', () => { describe('codeBlock', () => { @@ -183,7 +184,7 @@ describe('Message formatters', () => { describe('time', () => { test('GIVEN no arguments THEN returns ""', () => { vitest.useFakeTimers(); - vitest.setSystemTime(1566424897579); + vitest.setSystemTime(1_566_424_897_579); expect<``>(time()).toEqual(''); @@ -191,29 +192,29 @@ describe('Message formatters', () => { }); test('GIVEN a date THEN returns ""', () => { - expect<``>(time(new Date(1867424897579))).toEqual(''); + expect<``>(time(new Date(1_867_424_897_579))).toEqual(''); }); test('GIVEN a date and a style from string THEN returns ""', () => { - expect<``>(time(new Date(1867424897579), 'd')).toEqual(''); + expect<``>(time(new Date(1_867_424_897_579), 'd')).toEqual(''); }); test('GIVEN a date and a format from enum THEN returns ""', () => { - expect<``>(time(new Date(1867424897579), TimestampStyles.RelativeTime)).toEqual( + expect<``>(time(new Date(1_867_424_897_579), TimestampStyles.RelativeTime)).toEqual( '', ); }); test('GIVEN a date THEN returns ""', () => { - expect<''>(time(1867424897)).toEqual(''); + expect<''>(time(1_867_424_897)).toEqual(''); }); test('GIVEN a date and a style from string THEN returns ""', () => { - expect<''>(time(1867424897, 'd')).toEqual(''); + expect<''>(time(1_867_424_897, 'd')).toEqual(''); }); test('GIVEN a date and a format from enum THEN returns ""', () => { - expect<''>(time(1867424897, TimestampStyles.RelativeTime)).toEqual(''); + expect<''>(time(1_867_424_897, TimestampStyles.RelativeTime)).toEqual(''); }); }); diff --git a/packages/builders/__tests__/util.test.ts b/packages/builders/__tests__/util.test.ts index 893345754dec..abe1431d5c44 100644 --- a/packages/builders/__tests__/util.test.ts +++ b/packages/builders/__tests__/util.test.ts @@ -6,7 +6,7 @@ import { enableValidators, disableValidators, isValidationEnabled, -} from '../src/index'; +} from '../src/index.js'; describe('isEquatable', () => { test('returns true if the object is equatable', () => { diff --git a/packages/builders/package.json b/packages/builders/package.json index ebee5ebeee39..ac914e8e53cd 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -65,16 +65,10 @@ "@favware/cliff-jumper": "^1.8.7", "@microsoft/api-extractor": "^7.29.5", "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@vitest/coverage-c8": "^0.22.1", "downlevel-dts": "^0.10.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-tsdoc": "^0.2.16", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 245fa1dc4984..3a74a21c72be 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -1,21 +1,21 @@ import { type APIActionRowComponent, ComponentType, - APIMessageActionRowComponent, - APIModalActionRowComponent, - APIActionRowComponentTypes, + type APIMessageActionRowComponent, + type APIModalActionRowComponent, + type APIActionRowComponentTypes, } from 'discord-api-types/v10'; -import { ComponentBuilder } from './Component'; -import { createComponentBuilder } from './Components'; +import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js'; +import { ComponentBuilder } from './Component.js'; +import { createComponentBuilder } from './Components.js'; import type { ButtonBuilder } from './button/Button'; import type { SelectMenuBuilder } from './selectMenu/SelectMenu'; import type { TextInputBuilder } from './textInput/TextInput'; -import { normalizeArray, type RestOrArray } from '../util/normalizeArray'; export type MessageComponentBuilder = - | MessageActionRowComponentBuilder - | ActionRowBuilder; -export type ModalComponentBuilder = ModalActionRowComponentBuilder | ActionRowBuilder; + | ActionRowBuilder + | MessageActionRowComponentBuilder; +export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder; export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder; export type ModalActionRowComponentBuilder = TextInputBuilder; export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder; @@ -35,7 +35,7 @@ export class ActionRowBuilder extends ComponentBu public constructor({ components, ...data }: Partial> = {}) { super({ type: ComponentType.ActionRow, ...data }); - this.components = (components?.map((c) => createComponentBuilder(c)) ?? []) as T[]; + this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as T[]; } /** diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 5fa6c7efceaf..faae51dd16b7 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -1,7 +1,7 @@ import { s } from '@sapphire/shapeshift'; -import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10'; -import { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption'; -import { isValidationEnabled } from '../util/validation'; +import { ButtonStyle, type APIMessageComponentEmoji } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../util/validation.js'; +import { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption.js'; export const customIdValidator = s.string .lengthGreaterThanOrEqual(1) diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index b9f85674af9b..d56e235dda4a 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -6,7 +6,7 @@ import type { } from 'discord-api-types/v10'; import type { JSONEncodable } from '../util/jsonEncodable'; -export type AnyAPIActionRowComponent = APIActionRowComponentTypes | APIActionRowComponent; +export type AnyAPIActionRowComponent = APIActionRowComponent | APIActionRowComponentTypes; /** * Represents a discord component diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index b45ae95e9ff2..4c73932f70b7 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -1,14 +1,14 @@ -import { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v10'; +import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10'; import { ActionRowBuilder, type AnyComponentBuilder, type MessageComponentBuilder, type ModalComponentBuilder, -} from './ActionRow'; -import { ComponentBuilder } from './Component'; -import { ButtonBuilder } from './button/Button'; -import { SelectMenuBuilder } from './selectMenu/SelectMenu'; -import { TextInputBuilder } from './textInput/TextInput'; +} from './ActionRow.js'; +import { ComponentBuilder } from './Component.js'; +import { ButtonBuilder } from './button/Button.js'; +import { SelectMenuBuilder } from './selectMenu/SelectMenu.js'; +import { TextInputBuilder } from './textInput/TextInput.js'; export interface MappedComponentTypes { [ComponentType.ActionRow]: ActionRowBuilder; @@ -23,7 +23,8 @@ export interface MappedComponentTypes { * @param data - The api data to transform to a component class */ export function createComponentBuilder( - data: (APIMessageComponent | APIModalComponent) & { type: T }, + // eslint-disable-next-line @typescript-eslint/sort-type-union-intersection-members + data: (APIModalComponent | APIMessageComponent) & { type: T }, ): MappedComponentTypes[T]; export function createComponentBuilder(data: C): C; export function createComponentBuilder( @@ -43,7 +44,7 @@ export function createComponentBuilder( case ComponentType.TextInput: return new TextInputBuilder(data); default: - // @ts-expect-error + // @ts-expect-error: This case can still occur if we get a newer unsupported component type // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Cannot properly serialize component type: ${data.type}`); } diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index 63ed260909f3..4b63002fbc87 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -1,10 +1,10 @@ import { ComponentType, - ButtonStyle, type APIMessageComponentEmoji, type APIButtonComponent, type APIButtonComponentWithURL, type APIButtonComponentWithCustomId, + type ButtonStyle, } from 'discord-api-types/v10'; import { buttonLabelValidator, @@ -14,8 +14,8 @@ import { emojiValidator, urlValidator, validateRequiredButtonParameters, -} from '../Assertions'; -import { ComponentBuilder } from '../Component'; +} from '../Assertions.js'; +import { ComponentBuilder } from '../Component.js'; /** * Represents a button component @@ -23,8 +23,8 @@ import { ComponentBuilder } from '../Component'; export class ButtonBuilder extends ComponentBuilder { /** * Creates a new button from API data - * @param data - The API data to create this button with * + * @param data - The API data to create this button with * @example * Creating a button from an API data object * ```ts @@ -38,7 +38,6 @@ export class ButtonBuilder extends ComponentBuilder { * custom_id: '12345678901234567890123456789012', * }); * ``` - * * @example * Creating a button using setters and API data * ```ts @@ -70,7 +69,6 @@ export class ButtonBuilder extends ComponentBuilder { * @remarks * This method is only available to buttons using the `Link` button style. * Only three types of URL schemes are currently supported: `https://`, `http://` and `discord://` - * * @param url - The URL to open when this button is clicked */ public setURL(url: string) { @@ -83,7 +81,6 @@ export class ButtonBuilder extends ComponentBuilder { * * @remarks * This method is only applicable to buttons that are not using the `Link` button style. - * * @param customId - The custom id to use for this button */ public setCustomId(customId: string) { diff --git a/packages/builders/src/components/selectMenu/SelectMenu.ts b/packages/builders/src/components/selectMenu/SelectMenu.ts index 55751ee70938..39fdfb6d688b 100644 --- a/packages/builders/src/components/selectMenu/SelectMenu.ts +++ b/packages/builders/src/components/selectMenu/SelectMenu.ts @@ -1,6 +1,5 @@ -import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v10'; -import { SelectMenuOptionBuilder } from './SelectMenuOption'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; +import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { customIdValidator, disabledValidator, @@ -9,8 +8,9 @@ import { optionsLengthValidator, placeholderValidator, validateRequiredSelectMenuParameters, -} from '../Assertions'; -import { ComponentBuilder } from '../Component'; +} from '../Assertions.js'; +import { ComponentBuilder } from '../Component.js'; +import { SelectMenuOptionBuilder } from './SelectMenuOption.js'; /** * Represents a select menu component @@ -24,7 +24,7 @@ export class SelectMenuBuilder extends ComponentBuilder public constructor(data?: Partial) { const { options, ...initData } = data ?? {}; super({ type: ComponentType.SelectMenu, ...initData }); - this.options = options?.map((o) => new SelectMenuOptionBuilder(o)) ?? []; + this.options = options?.map((option) => new SelectMenuOptionBuilder(option)) ?? []; } /** @@ -83,7 +83,8 @@ export class SelectMenuBuilder extends ComponentBuilder * @param options - The options to add to this select menu * @returns */ - public addOptions(...options: RestOrArray) { + public addOptions(...options: RestOrArray) { + // eslint-disable-next-line no-param-reassign options = normalizeArray(options); optionsLengthValidator.parse(this.options.length + options.length); this.options.push( @@ -101,7 +102,8 @@ export class SelectMenuBuilder extends ComponentBuilder * * @param options - The options to set on this select menu */ - public setOptions(...options: RestOrArray) { + public setOptions(...options: RestOrArray) { + // eslint-disable-next-line no-param-reassign options = normalizeArray(options); optionsLengthValidator.parse(options.length); this.options.splice( @@ -124,7 +126,7 @@ export class SelectMenuBuilder extends ComponentBuilder // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...this.data, - options: this.options.map((o) => o.toJSON()), + options: this.options.map((option) => option.toJSON()), } as APISelectMenuComponent; } } diff --git a/packages/builders/src/components/selectMenu/SelectMenuOption.ts b/packages/builders/src/components/selectMenu/SelectMenuOption.ts index 3dba43869373..26edf23dc6a0 100644 --- a/packages/builders/src/components/selectMenu/SelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/SelectMenuOption.ts @@ -1,12 +1,11 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10'; -import type { JSONEncodable } from '../../util/jsonEncodable'; - +import type { JSONEncodable } from '../../util/jsonEncodable.js'; import { defaultValidator, emojiValidator, labelValueDescriptionValidator, validateRequiredSelectMenuOptionParameters, -} from '../Assertions'; +} from '../Assertions.js'; /** * Represents a option within a select menu component diff --git a/packages/builders/src/components/textInput/Assertions.ts b/packages/builders/src/components/textInput/Assertions.ts index b468c0029a30..f0cfc4a3f802 100644 --- a/packages/builders/src/components/textInput/Assertions.ts +++ b/packages/builders/src/components/textInput/Assertions.ts @@ -1,19 +1,19 @@ import { s } from '@sapphire/shapeshift'; import { TextInputStyle } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation'; -import { customIdValidator } from '../Assertions'; +import { isValidationEnabled } from '../../util/validation.js'; +import { customIdValidator } from '../Assertions.js'; export const textInputStyleValidator = s.nativeEnum(TextInputStyle); export const minLengthValidator = s.number.int .greaterThanOrEqual(0) - .lessThanOrEqual(4000) + .lessThanOrEqual(4_000) .setValidationEnabled(isValidationEnabled); export const maxLengthValidator = s.number.int .greaterThanOrEqual(1) - .lessThanOrEqual(4000) + .lessThanOrEqual(4_000) .setValidationEnabled(isValidationEnabled); export const requiredValidator = s.boolean; -export const valueValidator = s.string.lengthLessThanOrEqual(4000).setValidationEnabled(isValidationEnabled); +export const valueValidator = s.string.lengthLessThanOrEqual(4_000).setValidationEnabled(isValidationEnabled); export const placeholderValidator = s.string.lengthLessThanOrEqual(100).setValidationEnabled(isValidationEnabled); export const labelValidator = s.string .lengthGreaterThanOrEqual(1) diff --git a/packages/builders/src/components/textInput/TextInput.ts b/packages/builders/src/components/textInput/TextInput.ts index 9bcbe7c41b6c..4c68a0e20b1f 100644 --- a/packages/builders/src/components/textInput/TextInput.ts +++ b/packages/builders/src/components/textInput/TextInput.ts @@ -1,5 +1,9 @@ import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10'; import isEqual from 'fast-deep-equal'; +import type { Equatable } from '../../util/equatable'; +import { isJSONEncodable, type JSONEncodable } from '../../util/jsonEncodable.js'; +import { customIdValidator } from '../Assertions.js'; +import { ComponentBuilder } from '../Component.js'; import { maxLengthValidator, minLengthValidator, @@ -9,15 +13,11 @@ import { validateRequiredParameters, labelValidator, textInputStyleValidator, -} from './Assertions'; -import type { Equatable } from '../../util/equatable'; -import { isJSONEncodable, type JSONEncodable } from '../../util/jsonEncodable'; -import { customIdValidator } from '../Assertions'; -import { ComponentBuilder } from '../Component'; +} from './Assertions.js'; export class TextInputBuilder extends ComponentBuilder - implements Equatable | APITextInputComponent> + implements Equatable> { public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) { super({ type: ComponentType.TextInput, ...data }); @@ -117,7 +117,7 @@ export class TextInputBuilder /** * {@inheritDoc Equatable.equals} */ - public equals(other: JSONEncodable | APITextInputComponent): boolean { + public equals(other: APITextInputComponent | JSONEncodable): boolean { if (isJSONEncodable(other)) { return isEqual(other.toJSON(), this.data); } diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index ba087db94133..58c0a5f57928 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -1,43 +1,43 @@ -export * as EmbedAssertions from './messages/embed/Assertions'; -export * from './messages/embed/Embed'; -export * from './messages/formatters'; +export * as EmbedAssertions from './messages/embed/Assertions.js'; +export * from './messages/embed/Embed.js'; +export * from './messages/formatters.js'; -export * as ComponentAssertions from './components/Assertions'; -export * from './components/ActionRow'; -export * from './components/button/Button'; -export * from './components/Component'; -export * from './components/Components'; -export * from './components/textInput/TextInput'; -export * as TextInputAssertions from './components/textInput/Assertions'; -export * from './interactions/modals/Modal'; -export * as ModalAssertions from './interactions/modals/Assertions'; -export * from './components/selectMenu/SelectMenu'; -export * from './components/selectMenu/SelectMenuOption'; +export * as ComponentAssertions from './components/Assertions.js'; +export * from './components/ActionRow.js'; +export * from './components/button/Button.js'; +export * from './components/Component.js'; +export * from './components/Components.js'; +export * from './components/textInput/TextInput.js'; +export * as TextInputAssertions from './components/textInput/Assertions.js'; +export * from './interactions/modals/Modal.js'; +export * as ModalAssertions from './interactions/modals/Assertions.js'; +export * from './components/selectMenu/SelectMenu.js'; +export * from './components/selectMenu/SelectMenuOption.js'; -export * as SlashCommandAssertions from './interactions/slashCommands/Assertions'; -export * from './interactions/slashCommands/SlashCommandBuilder'; -export * from './interactions/slashCommands/SlashCommandSubcommands'; -export * from './interactions/slashCommands/options/boolean'; -export * from './interactions/slashCommands/options/channel'; -export * from './interactions/slashCommands/options/integer'; -export * from './interactions/slashCommands/options/mentionable'; -export * from './interactions/slashCommands/options/number'; -export * from './interactions/slashCommands/options/role'; -export * from './interactions/slashCommands/options/attachment'; -export * from './interactions/slashCommands/options/string'; -export * from './interactions/slashCommands/options/user'; -export * from './interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin'; -export * from './interactions/slashCommands/mixins/ApplicationCommandOptionBase'; -export * from './interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin'; -export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin'; -export * from './interactions/slashCommands/mixins/NameAndDescription'; -export * from './interactions/slashCommands/mixins/SharedSlashCommandOptions'; +export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js'; +export * from './interactions/slashCommands/SlashCommandBuilder.js'; +export * from './interactions/slashCommands/SlashCommandSubcommands.js'; +export * from './interactions/slashCommands/options/boolean.js'; +export * from './interactions/slashCommands/options/channel.js'; +export * from './interactions/slashCommands/options/integer.js'; +export * from './interactions/slashCommands/options/mentionable.js'; +export * from './interactions/slashCommands/options/number.js'; +export * from './interactions/slashCommands/options/role.js'; +export * from './interactions/slashCommands/options/attachment.js'; +export * from './interactions/slashCommands/options/string.js'; +export * from './interactions/slashCommands/options/user.js'; +export * from './interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; +export * from './interactions/slashCommands/mixins/ApplicationCommandOptionBase.js'; +export * from './interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.js'; +export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.js'; +export * from './interactions/slashCommands/mixins/NameAndDescription.js'; +export * from './interactions/slashCommands/mixins/SharedSlashCommandOptions.js'; -export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions'; -export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder'; +export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions.js'; +export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder.js'; -export * from './util/jsonEncodable'; -export * from './util/equatable'; -export * from './util/componentUtil'; -export * from './util/normalizeArray'; -export * from './util/validation'; +export * from './util/jsonEncodable.js'; +export * from './util/equatable.js'; +export * from './util/componentUtil.js'; +export * from './util/normalizeArray.js'; +export * from './util/validation.js'; diff --git a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts index af5ffa9b114b..7f65a0344e5b 100644 --- a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts +++ b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts @@ -1,11 +1,12 @@ import { s } from '@sapphire/shapeshift'; import { ApplicationCommandType } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation.js'; import type { ContextMenuCommandType } from './ContextMenuCommandBuilder'; -import { isValidationEnabled } from '../../util/validation'; const namePredicate = s.string .lengthGreaterThanOrEqual(1) .lengthLessThanOrEqual(32) + // eslint-disable-next-line prefer-named-capture-group, unicorn/no-unsafe-regex .regex(/^( *[\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+ *)+$/u) .setValidationEnabled(isValidationEnabled); const typePredicate = s diff --git a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts b/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts index 53c6ae70a268..4deb42a26036 100644 --- a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts +++ b/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts @@ -5,6 +5,7 @@ import type { Permissions, RESTPostAPIApplicationCommandsJSONBody, } from 'discord-api-types/v10'; +import { validateLocale, validateLocalizationMap } from '../slashCommands/Assertions.js'; import { validateRequiredParameters, validateName, @@ -12,8 +13,7 @@ import { validateDefaultPermission, validateDefaultMemberPermissions, validateDMPermission, -} from './Assertions'; -import { validateLocale, validateLocalizationMap } from '../slashCommands/Assertions'; +} from './Assertions.js'; export class ContextMenuCommandBuilder { /** @@ -84,7 +84,6 @@ export class ContextMenuCommandBuilder { * **Note**: If set to `false`, you will have to later `PUT` the permissions for this command. * * @param value - Whether or not to enable this command by default - * * @see https://discord.com/developers/docs/interactions/application-commands#permissions * @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead. */ @@ -103,7 +102,6 @@ export class ContextMenuCommandBuilder { * **Note:** You can set this to `'0'` to disable the command by default. * * @param permissions - The permissions bit field to set - * * @see https://discord.com/developers/docs/interactions/application-commands#permissions */ public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) { @@ -120,7 +118,6 @@ export class ContextMenuCommandBuilder { * By default, commands are visible. * * @param enabled - If the command should be enabled in DMs - * * @see https://discord.com/developers/docs/interactions/application-commands#permissions */ public setDMPermission(enabled: boolean | null | undefined) { @@ -169,9 +166,8 @@ export class ContextMenuCommandBuilder { Reflect.set(this, 'name_localizations', {}); - Object.entries(localizedNames).forEach((args) => - this.setNameLocalization(...(args as [LocaleString, string | null])), - ); + for (const args of Object.entries(localizedNames)) + this.setNameLocalization(...(args as [LocaleString, string | null])); return this; } @@ -189,4 +185,4 @@ export class ContextMenuCommandBuilder { } } -export type ContextMenuCommandType = ApplicationCommandType.User | ApplicationCommandType.Message; +export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User; diff --git a/packages/builders/src/interactions/modals/Assertions.ts b/packages/builders/src/interactions/modals/Assertions.ts index 2b972f23d3a3..a59bb24cfe62 100644 --- a/packages/builders/src/interactions/modals/Assertions.ts +++ b/packages/builders/src/interactions/modals/Assertions.ts @@ -1,7 +1,7 @@ import { s } from '@sapphire/shapeshift'; -import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow'; -import { customIdValidator } from '../../components/Assertions'; -import { isValidationEnabled } from '../../util/validation'; +import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js'; +import { customIdValidator } from '../../components/Assertions.js'; +import { isValidationEnabled } from '../../util/validation.js'; export const titleValidator = s.string .lengthGreaterThanOrEqual(1) diff --git a/packages/builders/src/interactions/modals/Modal.ts b/packages/builders/src/interactions/modals/Modal.ts index d320b8958870..db0c619a6ab3 100644 --- a/packages/builders/src/interactions/modals/Modal.ts +++ b/packages/builders/src/interactions/modals/Modal.ts @@ -3,20 +3,21 @@ import type { APIModalActionRowComponent, APIModalInteractionResponseCallbackData, } from 'discord-api-types/v10'; -import { titleValidator, validateRequiredParameters } from './Assertions'; -import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow'; -import { customIdValidator } from '../../components/Assertions'; -import { createComponentBuilder } from '../../components/Components'; +import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js'; +import { customIdValidator } from '../../components/Assertions.js'; +import { createComponentBuilder } from '../../components/Components.js'; import type { JSONEncodable } from '../../util/jsonEncodable'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; +import { titleValidator, validateRequiredParameters } from './Assertions.js'; export class ModalBuilder implements JSONEncodable { public readonly data: Partial; + public readonly components: ActionRowBuilder[] = []; public constructor({ components, ...data }: Partial = {}) { this.data = { ...data }; - this.components = (components?.map((c) => createComponentBuilder(c)) ?? + this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ActionRowBuilder[]; } diff --git a/packages/builders/src/interactions/slashCommands/Assertions.ts b/packages/builders/src/interactions/slashCommands/Assertions.ts index 27c8fc5ed580..7a251a616583 100644 --- a/packages/builders/src/interactions/slashCommands/Assertions.ts +++ b/packages/builders/src/interactions/slashCommands/Assertions.ts @@ -1,9 +1,9 @@ import { s } from '@sapphire/shapeshift'; -import { type APIApplicationCommandOptionChoice, Locale, LocalizationMap } from 'discord-api-types/v10'; +import { Locale, type APIApplicationCommandOptionChoice, type LocalizationMap } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation.js'; import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder'; import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands'; import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase'; -import { isValidationEnabled } from '../../util/validation'; const namePredicate = s.string .lengthGreaterThanOrEqual(1) diff --git a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts index 39c19575dfa2..fdfa4d3c1fa8 100644 --- a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts +++ b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts @@ -13,10 +13,10 @@ import { validateDMPermission, validateMaxOptionsLength, validateRequiredParameters, -} from './Assertions'; -import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands'; -import { SharedNameAndDescription } from './mixins/NameAndDescription'; -import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions'; +} from './Assertions.js'; +import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands.js'; +import { SharedNameAndDescription } from './mixins/NameAndDescription.js'; +import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js'; @mix(SharedSlashCommandOptions, SharedNameAndDescription) export class SlashCommandBuilder { @@ -87,7 +87,6 @@ export class SlashCommandBuilder { * **Note**: If set to `false`, you will have to later `PUT` the permissions for this command. * * @param value - Whether or not to enable this command by default - * * @see https://discord.com/developers/docs/interactions/application-commands#permissions * @deprecated Use {@link (SlashCommandBuilder:class).setDefaultMemberPermissions} or {@link (SlashCommandBuilder:class).setDMPermission} instead. */ @@ -106,7 +105,6 @@ export class SlashCommandBuilder { * **Note:** You can set this to `'0'` to disable the command by default. * * @param permissions - The permissions bit field to set - * * @see https://discord.com/developers/docs/interactions/application-commands#permissions */ public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) { @@ -123,7 +121,6 @@ export class SlashCommandBuilder { * By default, commands are visible. * * @param enabled - If the command should be enabled in DMs - * * @see https://discord.com/developers/docs/interactions/application-commands#permissions */ public setDMPermission(enabled: boolean | null | undefined) { @@ -192,7 +189,7 @@ export interface SlashCommandBuilder extends SharedNameAndDescription, SharedSla export interface SlashCommandSubcommandsOnlyBuilder extends SharedNameAndDescription, - Pick {} + Pick {} export interface SlashCommandOptionsOnlyBuilder extends SharedNameAndDescription, @@ -200,5 +197,5 @@ export interface SlashCommandOptionsOnlyBuilder Pick {} export interface ToAPIApplicationCommandOptions { - toJSON: () => APIApplicationCommandOption; + toJSON(): APIApplicationCommandOption; } diff --git a/packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts b/packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts index 369c3556ab65..22550b26347e 100644 --- a/packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts +++ b/packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts @@ -1,14 +1,14 @@ import { - APIApplicationCommandSubcommandGroupOption, - APIApplicationCommandSubcommandOption, ApplicationCommandOptionType, + type APIApplicationCommandSubcommandGroupOption, + type APIApplicationCommandSubcommandOption, } from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; -import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions'; -import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder'; +import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions.js'; +import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder.js'; import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase'; -import { SharedNameAndDescription } from './mixins/NameAndDescription'; -import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions'; +import { SharedNameAndDescription } from './mixins/NameAndDescription.js'; +import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js'; /** * Represents a folder for subcommands diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts index ae107058bc79..5ac38d1f6a89 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts @@ -1,5 +1,6 @@ export abstract class ApplicationCommandNumericOptionMinMaxValueMixin { public readonly max_value?: number; + public readonly min_value?: number; /** diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts index beba4af33408..b40ce0c96934 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts @@ -1,6 +1,6 @@ import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { SharedNameAndDescription } from './NameAndDescription'; -import { validateRequiredParameters, validateRequired, validateLocalizationMap } from '../Assertions'; +import { validateRequiredParameters, validateRequired, validateLocalizationMap } from '../Assertions.js'; +import { SharedNameAndDescription } from './NameAndDescription.js'; export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription { public abstract readonly type: ApplicationCommandOptionType; diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts index 76a2dcb408f0..5ea7105f14c4 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts @@ -1,9 +1,9 @@ import { s } from '@sapphire/shapeshift'; -import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { localizationMapPredicate, validateChoicesLength } from '../Assertions'; +import { ApplicationCommandOptionType, type APIApplicationCommandOptionChoice } from 'discord-api-types/v10'; +import { localizationMapPredicate, validateChoicesLength } from '../Assertions.js'; const stringPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100); -const numberPredicate = s.number.greaterThan(-Infinity).lessThan(Infinity); +const numberPredicate = s.number.greaterThan(Number.NEGATIVE_INFINITY).lessThan(Number.POSITIVE_INFINITY); const choicesPredicate = s.object({ name: stringPredicate, name_localizations: localizationMapPredicate, @@ -11,8 +11,9 @@ const choicesPredicate = s.object({ }).array; const booleanPredicate = s.boolean; -export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin { +export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin { public readonly choices?: APIApplicationCommandOptionChoice[]; + public readonly autocomplete?: boolean; // Since this is present and this is a mixin, this is needed @@ -65,6 +66,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin - this.setNameLocalization(...(args as [LocaleString, string | null])), - ); + for (const args of Object.entries(localizedNames)) { + this.setNameLocalization(...(args as [LocaleString, string | null])); + } + return this; } @@ -114,9 +118,10 @@ export class SharedNameAndDescription { } Reflect.set(this, 'description_localizations', {}); - Object.entries(localizedDescriptions).forEach((args) => - this.setDescriptionLocalization(...(args as [LocaleString, string | null])), - ); + for (const args of Object.entries(localizedDescriptions)) { + this.setDescriptionLocalization(...(args as [LocaleString, string | null])); + } + return this; } } diff --git a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts index 6acf9becea9b..db278d83dd13 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts @@ -1,15 +1,15 @@ -import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase'; -import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions'; +import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions.js'; import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder'; -import { SlashCommandAttachmentOption } from '../options/attachment'; -import { SlashCommandBooleanOption } from '../options/boolean'; -import { SlashCommandChannelOption } from '../options/channel'; -import { SlashCommandIntegerOption } from '../options/integer'; -import { SlashCommandMentionableOption } from '../options/mentionable'; -import { SlashCommandNumberOption } from '../options/number'; -import { SlashCommandRoleOption } from '../options/role'; -import { SlashCommandStringOption } from '../options/string'; -import { SlashCommandUserOption } from '../options/user'; +import { SlashCommandAttachmentOption } from '../options/attachment.js'; +import { SlashCommandBooleanOption } from '../options/boolean.js'; +import { SlashCommandChannelOption } from '../options/channel.js'; +import { SlashCommandIntegerOption } from '../options/integer.js'; +import { SlashCommandMentionableOption } from '../options/mentionable.js'; +import { SlashCommandNumberOption } from '../options/number.js'; +import { SlashCommandRoleOption } from '../options/role.js'; +import { SlashCommandStringOption } from '../options/string.js'; +import { SlashCommandUserOption } from '../options/user.js'; +import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; export class SharedSlashCommandOptions { public readonly options!: ToAPIApplicationCommandOptions[]; @@ -83,15 +83,15 @@ export class SharedSlashCommandOptions { */ public addStringOption( input: - | SlashCommandStringOption - | Omit | Omit + | Omit + | SlashCommandStringOption | (( builder: SlashCommandStringOption, ) => - | SlashCommandStringOption + | Omit | Omit - | Omit), + | SlashCommandStringOption), ) { return this._sharedAddOptionMethod(input, SlashCommandStringOption); } @@ -103,15 +103,15 @@ export class SharedSlashCommandOptions { */ public addIntegerOption( input: - | SlashCommandIntegerOption - | Omit | Omit + | Omit + | SlashCommandIntegerOption | (( builder: SlashCommandIntegerOption, ) => - | SlashCommandIntegerOption + | Omit | Omit - | Omit), + | SlashCommandIntegerOption), ) { return this._sharedAddOptionMethod(input, SlashCommandIntegerOption); } @@ -123,25 +123,25 @@ export class SharedSlashCommandOptions { */ public addNumberOption( input: - | SlashCommandNumberOption - | Omit | Omit + | Omit + | SlashCommandNumberOption | (( builder: SlashCommandNumberOption, ) => - | SlashCommandNumberOption + | Omit | Omit - | Omit), + | SlashCommandNumberOption), ) { return this._sharedAddOptionMethod(input, SlashCommandNumberOption); } private _sharedAddOptionMethod( input: - | T - | Omit | Omit - | ((builder: T) => T | Omit | Omit), + | Omit + | T + | ((builder: T) => Omit | Omit | T), Instance: new () => T, ): ShouldOmitSubcommandFunctions extends true ? Omit : this { const { options } = this; diff --git a/packages/builders/src/interactions/slashCommands/options/attachment.ts b/packages/builders/src/interactions/slashCommands/options/attachment.ts index e590e46292f1..006911033f78 100644 --- a/packages/builders/src/interactions/slashCommands/options/attachment.ts +++ b/packages/builders/src/interactions/slashCommands/options/attachment.ts @@ -1,5 +1,5 @@ -import { APIApplicationCommandAttachmentOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionType, type APIApplicationCommandAttachmentOption } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; export class SlashCommandAttachmentOption extends ApplicationCommandOptionBase { public override readonly type = ApplicationCommandOptionType.Attachment as const; diff --git a/packages/builders/src/interactions/slashCommands/options/boolean.ts b/packages/builders/src/interactions/slashCommands/options/boolean.ts index 0faaee592429..f2c9768bad6e 100644 --- a/packages/builders/src/interactions/slashCommands/options/boolean.ts +++ b/packages/builders/src/interactions/slashCommands/options/boolean.ts @@ -1,5 +1,5 @@ -import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionType, type APIApplicationCommandBooleanOption } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; export class SlashCommandBooleanOption extends ApplicationCommandOptionBase { public readonly type = ApplicationCommandOptionType.Boolean as const; diff --git a/packages/builders/src/interactions/slashCommands/options/channel.ts b/packages/builders/src/interactions/slashCommands/options/channel.ts index 1e5111aef90d..e3dac0aa6c15 100644 --- a/packages/builders/src/interactions/slashCommands/options/channel.ts +++ b/packages/builders/src/interactions/slashCommands/options/channel.ts @@ -1,7 +1,7 @@ -import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; +import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js'; @mix(ApplicationCommandOptionChannelTypesMixin) export class SlashCommandChannelOption extends ApplicationCommandOptionBase { diff --git a/packages/builders/src/interactions/slashCommands/options/integer.ts b/packages/builders/src/interactions/slashCommands/options/integer.ts index a2276e2ba953..e8a98f4c44d0 100644 --- a/packages/builders/src/interactions/slashCommands/options/integer.ts +++ b/packages/builders/src/interactions/slashCommands/options/integer.ts @@ -1,9 +1,9 @@ import { s } from '@sapphire/shapeshift'; -import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionType, type APIApplicationCommandIntegerOption } from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; -import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin'; +import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; +import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.js'; const numberValidator = s.number.int; diff --git a/packages/builders/src/interactions/slashCommands/options/mentionable.ts b/packages/builders/src/interactions/slashCommands/options/mentionable.ts index be7416f1437c..91a0416dfd62 100644 --- a/packages/builders/src/interactions/slashCommands/options/mentionable.ts +++ b/packages/builders/src/interactions/slashCommands/options/mentionable.ts @@ -1,5 +1,5 @@ -import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionType, type APIApplicationCommandMentionableOption } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; export class SlashCommandMentionableOption extends ApplicationCommandOptionBase { public readonly type = ApplicationCommandOptionType.Mentionable as const; diff --git a/packages/builders/src/interactions/slashCommands/options/number.ts b/packages/builders/src/interactions/slashCommands/options/number.ts index 6dc820633702..80b5cd6e5593 100644 --- a/packages/builders/src/interactions/slashCommands/options/number.ts +++ b/packages/builders/src/interactions/slashCommands/options/number.ts @@ -1,9 +1,9 @@ import { s } from '@sapphire/shapeshift'; -import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionType, type APIApplicationCommandNumberOption } from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; -import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin'; +import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; +import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.js'; const numberValidator = s.number; diff --git a/packages/builders/src/interactions/slashCommands/options/role.ts b/packages/builders/src/interactions/slashCommands/options/role.ts index 85dc376536cf..4f5871d56069 100644 --- a/packages/builders/src/interactions/slashCommands/options/role.ts +++ b/packages/builders/src/interactions/slashCommands/options/role.ts @@ -1,5 +1,5 @@ -import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionType, type APIApplicationCommandRoleOption } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; export class SlashCommandRoleOption extends ApplicationCommandOptionBase { public override readonly type = ApplicationCommandOptionType.Role as const; diff --git a/packages/builders/src/interactions/slashCommands/options/string.ts b/packages/builders/src/interactions/slashCommands/options/string.ts index 15d4cbaa9b48..345b7934a12a 100644 --- a/packages/builders/src/interactions/slashCommands/options/string.ts +++ b/packages/builders/src/interactions/slashCommands/options/string.ts @@ -1,16 +1,18 @@ import { s } from '@sapphire/shapeshift'; -import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10'; import { mix } from 'ts-mixer'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; +import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.js'; -const minLengthValidator = s.number.greaterThanOrEqual(0).lessThanOrEqual(6000); -const maxLengthValidator = s.number.greaterThanOrEqual(1).lessThanOrEqual(6000); +const minLengthValidator = s.number.greaterThanOrEqual(0).lessThanOrEqual(6_000); +const maxLengthValidator = s.number.greaterThanOrEqual(1).lessThanOrEqual(6_000); @mix(ApplicationCommandOptionWithChoicesAndAutocompleteMixin) export class SlashCommandStringOption extends ApplicationCommandOptionBase { public readonly type = ApplicationCommandOptionType.String as const; + public readonly max_length?: number; + public readonly min_length?: number; /** diff --git a/packages/builders/src/interactions/slashCommands/options/user.ts b/packages/builders/src/interactions/slashCommands/options/user.ts index 9a750067daba..609450fa5d11 100644 --- a/packages/builders/src/interactions/slashCommands/options/user.ts +++ b/packages/builders/src/interactions/slashCommands/options/user.ts @@ -1,5 +1,5 @@ -import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionType, type APIApplicationCommandUserOption } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; export class SlashCommandUserOption extends ApplicationCommandOptionBase { public readonly type = ApplicationCommandOptionType.User as const; diff --git a/packages/builders/src/messages/embed/Assertions.ts b/packages/builders/src/messages/embed/Assertions.ts index 909575d8891b..fe2bcb308274 100644 --- a/packages/builders/src/messages/embed/Assertions.ts +++ b/packages/builders/src/messages/embed/Assertions.ts @@ -1,6 +1,6 @@ import { s } from '@sapphire/shapeshift'; import type { APIEmbedField } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation'; +import { isValidationEnabled } from '../../util/validation.js'; export const fieldNamePredicate = s.string .lengthGreaterThanOrEqual(1) @@ -9,7 +9,7 @@ export const fieldNamePredicate = s.string export const fieldValuePredicate = s.string .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(1024) + .lengthLessThanOrEqual(1_024) .setValidationEnabled(isValidationEnabled); export const fieldInlinePredicate = s.boolean.optional; @@ -64,12 +64,12 @@ export const colorPredicate = s.number.int export const descriptionPredicate = s.string .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(4096) + .lengthLessThanOrEqual(4_096) .nullable.setValidationEnabled(isValidationEnabled); export const footerTextPredicate = s.string .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(2048) + .lengthLessThanOrEqual(2_048) .nullable.setValidationEnabled(isValidationEnabled); export const embedFooterPredicate = s diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index eece585db33e..c6ad12187891 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -1,4 +1,5 @@ import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { colorPredicate, descriptionPredicate, @@ -10,8 +11,7 @@ import { titlePredicate, urlPredicate, validateFieldLength, -} from './Assertions'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray'; +} from './Assertions.js'; export type RGBTuple = [red: number, green: number, blue: number]; @@ -26,11 +26,11 @@ export interface IconData { proxyIconURL?: string; } -export type EmbedAuthorData = Omit & IconData; +export type EmbedAuthorData = IconData & Omit; export type EmbedAuthorOptions = Omit; -export type EmbedFooterData = Omit & IconData; +export type EmbedFooterData = IconData & Omit; export type EmbedFooterOptions = Omit; @@ -57,7 +57,6 @@ export class EmbedBuilder { * @remarks * This method accepts either an array of fields or a variable number of field parameters. * The maximum amount of fields that can be added is 25. - * * @example * Using an array * ```ts @@ -65,7 +64,6 @@ export class EmbedBuilder { * const embed = new EmbedBuilder() * .addFields(fields); * ``` - * * @example * Using rest parameters (variadic) * ```ts @@ -75,10 +73,10 @@ export class EmbedBuilder { * { name: 'Field 2', value: 'Value 2' }, * ); * ``` - * * @param fields - The fields to add */ public addFields(...fields: RestOrArray): this { + // eslint-disable-next-line no-param-reassign fields = normalizeArray(fields); // Ensure adding these fields won't exceed the 25 field limit validateFieldLength(fields.length, this.data.fields); @@ -100,26 +98,22 @@ export class EmbedBuilder { * The maximum amount of fields that can be added is 25. * * It's useful for modifying and adjusting order of the already-existing fields of an embed. - * * @example * Remove the first field * ```ts * embed.spliceFields(0, 1); * ``` - * * @example * Remove the first n fields * ```ts * const n = 4 * embed.spliceFields(0, n); * ``` - * * @example * Remove the last field * ```ts * embed.spliceFields(-1, 1); * ``` - * * @param index - The index to start at * @param deleteCount - The number of fields to remove * @param fields - The replacing field objects @@ -143,7 +137,6 @@ export class EmbedBuilder { * it splices the entire array of fields, replacing them with the provided fields. * * You can set a maximum of 25 fields. - * * @param fields - The fields to set */ public setFields(...fields: RestOrArray) { @@ -175,7 +168,7 @@ export class EmbedBuilder { * * @param color - The color of the embed */ - public setColor(color: number | RGBTuple | null): this { + public setColor(color: RGBTuple | number | null): this { // Data assertions colorPredicate.parse(color); @@ -184,6 +177,7 @@ export class EmbedBuilder { this.data.color = (red << 16) + (green << 8) + blue; return this; } + this.data.color = color ?? undefined; return this; } @@ -250,7 +244,7 @@ export class EmbedBuilder { * * @param timestamp - The timestamp or date */ - public setTimestamp(timestamp: number | Date | null = Date.now()): this { + public setTimestamp(timestamp: Date | number | null = Date.now()): this { // Data assertions timestampPredicate.parse(timestamp); diff --git a/packages/builders/src/messages/formatters.ts b/packages/builders/src/messages/formatters.ts index b2822c87730f..9396d36cfbba 100644 --- a/packages/builders/src/messages/formatters.ts +++ b/packages/builders/src/messages/formatters.ts @@ -1,4 +1,4 @@ -import type { URL } from 'url'; +import type { URL } from 'node:url'; import type { Snowflake } from 'discord-api-types/globals'; /** @@ -95,7 +95,7 @@ export function hideLinkEmbed(url: C): `<${C}>`; * @param url - The URL to wrap */ export function hideLinkEmbed(url: URL): `<${string}>`; -export function hideLinkEmbed(url: string | URL) { +export function hideLinkEmbed(url: URL | string) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `<${url}>`; } @@ -141,7 +141,7 @@ export function hyperlink( url: U, title: T, ): `[${C}](${U} "${T}")`; -export function hyperlink(content: string, url: string | URL, title?: string) { +export function hyperlink(content: string, url: URL | string, title?: string) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return title ? `[${content}](${url} "${title}")` : `[${content}](${url})`; } @@ -203,7 +203,7 @@ export function formatEmoji(emojiId: C, animated?: true): ` * @param emojiId - The emoji ID to format * @param animated - Whether the emoji is animated or not. Defaults to `false` */ -export function formatEmoji(emojiId: C, animated = false): `` | `<:_:${C}>` { +export function formatEmoji(emojiId: C, animated = false): `<:_:${C}>` | `` { return `<${animated ? 'a' : ''}:_:${emojiId}>`; } @@ -293,9 +293,10 @@ export function time(seconds: C): ``; * @param style - The style to use */ export function time(seconds: C, style: S): ``; -export function time(timeOrSeconds?: number | Date, style?: TimestampStylesString): string { +export function time(timeOrSeconds?: Date | number, style?: TimestampStylesString): string { if (typeof timeOrSeconds !== 'number') { - timeOrSeconds = Math.floor((timeOrSeconds?.getTime() ?? Date.now()) / 1000); + // eslint-disable-next-line no-param-reassign + timeOrSeconds = Math.floor((timeOrSeconds?.getTime() ?? Date.now()) / 1_000); } return typeof style === 'string' ? `` : ``; diff --git a/packages/builders/src/util/equatable.ts b/packages/builders/src/util/equatable.ts index 254853f54d8c..a308e6b8a967 100644 --- a/packages/builders/src/util/equatable.ts +++ b/packages/builders/src/util/equatable.ts @@ -8,11 +8,12 @@ export interface Equatable { /** * Whether or not this is equal to another structure */ - equals: (other: T) => boolean; + equals(other: T): boolean; } /** * Indicates if an object is equatable or not. + * * @param maybeEquatable - The object to check against */ export function isEquatable(maybeEquatable: unknown): maybeEquatable is Equatable { diff --git a/packages/builders/src/util/jsonEncodable.ts b/packages/builders/src/util/jsonEncodable.ts index 432719be8ebb..f32eb2f7817a 100644 --- a/packages/builders/src/util/jsonEncodable.ts +++ b/packages/builders/src/util/jsonEncodable.ts @@ -7,11 +7,12 @@ export interface JSONEncodable { /** * Transforms this object to its JSON format */ - toJSON: () => T; + toJSON(): T; } /** * Indicates if an object is encodable or not. + * * @param maybeEncodable - The object to check against */ export function isJSONEncodable(maybeEncodable: unknown): maybeEncodable is JSONEncodable { diff --git a/packages/collection/__tests__/collection.test.ts b/packages/collection/__tests__/collection.test.ts index f0fd3746d744..1ea89164f8b4 100644 --- a/packages/collection/__tests__/collection.test.ts +++ b/packages/collection/__tests__/collection.test.ts @@ -1,5 +1,7 @@ +/* eslint-disable unicorn/no-array-method-this-argument */ +/* eslint-disable id-length */ import { describe, test, expect } from 'vitest'; -import { Collection } from '../src'; +import { Collection } from '../src/index.js'; type TestCollection = Collection; @@ -131,9 +133,9 @@ describe('each() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.each()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.each(123), 123); }); @@ -152,7 +154,7 @@ describe('each() tests', () => { describe('ensure() tests', () => { test('throws if defaultValueGenerator is not a function', () => { const coll = createTestCollection(); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.ensure('d', 'abc'), 'abc'); }); @@ -176,7 +178,7 @@ describe('equals() tests', () => { const coll2 = createTestCollection(); test('returns false if no collection is passed', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expect(coll1.equals()).toBeFalsy(); }); @@ -198,9 +200,9 @@ describe('every() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.every()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.every(123), 123); }); @@ -224,9 +226,9 @@ describe('filter() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.filter()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.filter(123), 123); }); @@ -251,9 +253,9 @@ describe('find() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => createCollection().find()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => createCollection().find(123), 123); }); @@ -275,9 +277,9 @@ describe('findKey() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.findKey()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.findKey(123), 123); }); @@ -506,9 +508,9 @@ describe('map() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.map()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.map(123), 123); }); @@ -529,9 +531,9 @@ describe('mapValues() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.mapValues()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.mapValues(123), 123); }); @@ -606,9 +608,9 @@ describe('partition() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.partition()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.partition(123), 123); }); @@ -690,9 +692,9 @@ describe('reduce() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.reduce()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.reduce(123), 123); }); @@ -729,19 +731,15 @@ describe('some() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.some()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.some(123), 123); }); test('returns false if no items pass the predicate', () => { expect(coll.some((v) => v > 3)).toBeFalsy(); }); - - test('returns true if at least one item passes the predicate', () => { - expect(coll.some((x) => x === 2)).toBeTruthy(); - }); }); describe('sort() tests', () => { @@ -777,9 +775,9 @@ describe('sweep() test', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.sweep()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.sweep(123), 123); }); @@ -804,9 +802,9 @@ describe('tap() tests', () => { const coll = createTestCollection(); test('throws if fn is not a function', () => { - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.tap()); - // @ts-expect-error + // @ts-expect-error: invalid function expectInvalidFunctionError(() => coll.tap(123), 123); }); diff --git a/packages/collection/package.json b/packages/collection/package.json index 611936e2477d..7a7e2ff925f1 100644 --- a/packages/collection/package.json +++ b/packages/collection/package.json @@ -54,16 +54,10 @@ "@favware/cliff-jumper": "^1.8.7", "@microsoft/api-extractor": "^7.29.5", "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@vitest/coverage-c8": "^0.22.1", "downlevel-dts": "^0.10.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-tsdoc": "^0.2.16", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/collection/src/collection.ts b/packages/collection/src/collection.ts index ea2fb02d6eb6..edd837bebe2d 100644 --- a/packages/collection/src/collection.ts +++ b/packages/collection/src/collection.ts @@ -1,10 +1,12 @@ +/* eslint-disable id-length */ +/* eslint-disable no-param-reassign */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /** * @internal */ export interface CollectionConstructor { new (): Collection; - new (entries?: ReadonlyArray | null): Collection; + new (entries?: readonly (readonly [K, V])[] | null): Collection; new (iterable: Iterable): Collection; readonly prototype: Collection; readonly [Symbol.species]: CollectionConstructor; @@ -13,8 +15,11 @@ export interface CollectionConstructor { /** * Represents an immutable version of a collection */ -export type ReadonlyCollection = ReadonlyMap & - Omit, 'forEach' | 'ensure' | 'reverse' | 'sweep' | 'sort' | 'get' | 'set' | 'delete'>; +export type ReadonlyCollection = Omit< + Collection, + 'delete' | 'ensure' | 'forEach' | 'get' | 'reverse' | 'set' | 'sort' | 'sweep' +> & + ReadonlyMap; /** * Separate interface for the constructor so that emitted js does not have a constructor that overwrites itself @@ -38,7 +43,6 @@ export class Collection extends Map { * * @param key - The key to get if it exists, or set otherwise * @param defaultValueGenerator - A function that generates the default value - * * @example * ```ts * collection.ensure(guildId, () => defaultGuildConfig); @@ -56,7 +60,6 @@ export class Collection extends Map { * Checks if all of the elements exist in the collection. * * @param keys - The keys of the elements to check for - * * @returns `true` if all of the elements exist, `false` if at least one does not exist. */ public hasAll(...keys: K[]) { @@ -67,7 +70,6 @@ export class Collection extends Map { * Checks if any of the elements exist in the collection. * * @param keys - The keys of the elements to check for - * * @returns `true` if any of the elements exist, `false` if none exist. */ public hasAny(...keys: K[]) { @@ -78,7 +80,6 @@ export class Collection extends Map { * Obtains the first value(s) in this collection. * * @param amount - Amount of values to obtain from the beginning - * * @returns A single value if no amount is provided or an array of values, starting from the end if amount is negative */ public first(): V | undefined; @@ -97,7 +98,6 @@ export class Collection extends Map { * Obtains the first key(s) in this collection. * * @param amount - Amount of keys to obtain from the beginning - * * @returns A single key if no amount is provided or an array of keys, starting from the end if * amount is negative */ @@ -117,7 +117,6 @@ export class Collection extends Map { * Obtains the last value(s) in this collection. * * @param amount - Amount of values to obtain from the end - * * @returns A single value if no amount is provided or an array of values, starting from the start if * amount is negative */ @@ -135,7 +134,6 @@ export class Collection extends Map { * Obtains the last key(s) in this collection. * * @param amount - Amount of keys to obtain from the end - * * @returns A single key if no amount is provided or an array of keys, starting from the start if * amount is negative */ @@ -179,7 +177,6 @@ export class Collection extends Map { * Obtains unique random value(s) from this collection. * * @param amount - Amount of values to obtain randomly - * * @returns A single value if no amount is provided or an array of values */ public random(): V | undefined; @@ -198,7 +195,6 @@ export class Collection extends Map { * Obtains unique random key(s) from this collection. * * @param amount - Amount of keys to obtain randomly - * * @returns A single key if no amount is provided or an array */ public randomKey(): K | undefined; @@ -233,7 +229,6 @@ export class Collection extends Map { * * @param fn - The function to test with (should return boolean) * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.find(user => user.username === 'Bob'); @@ -252,6 +247,7 @@ export class Collection extends Map { for (const [key, val] of this) { if (fn(val, key, this)) return val; } + return undefined; } @@ -262,7 +258,6 @@ export class Collection extends Map { * * @param fn - The function to test with (should return boolean) * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.findKey(user => user.username === 'Bob'); @@ -281,6 +276,7 @@ export class Collection extends Map { for (const [key, val] of this) { if (fn(val, key, this)) return key; } + return undefined; } @@ -289,7 +285,6 @@ export class Collection extends Map { * * @param fn - Function used to test (should return a boolean) * @param thisArg - Value to use as `this` when executing function - * * @returns The number of removed entries */ public sweep(fn: (value: V, key: K, collection: this) => boolean): number; @@ -301,6 +296,7 @@ export class Collection extends Map { for (const [key, val] of this) { if (fn(val, key, this)) this.delete(key); } + return previousSize - this.size; } @@ -311,7 +307,6 @@ export class Collection extends Map { * * @param fn - The function to test with (should return boolean) * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.filter(user => user.username === 'Bob'); @@ -336,6 +331,7 @@ export class Collection extends Map { for (const [key, val] of this) { if (fn(val, key, this)) results.set(key, val); } + return results; } @@ -345,7 +341,6 @@ export class Collection extends Map { * * @param fn - Function used to test (should return a boolean) * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * const [big, small] = collection.partition(guild => guild.memberCount > 250); @@ -387,6 +382,7 @@ export class Collection extends Map { results[1].set(key, val); } } + return results; } @@ -396,7 +392,6 @@ export class Collection extends Map { * * @param fn - Function that produces a new Collection * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.flatMap(guild => guild.members.cache); @@ -408,6 +403,7 @@ export class Collection extends Map { thisArg: This, ): Collection; public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection { + // eslint-disable-next-line unicorn/no-array-method-this-argument const collections = this.map(fn, thisArg); return new this.constructor[Symbol.species]().concat(...collections); } @@ -418,7 +414,6 @@ export class Collection extends Map { * * @param fn - Function that produces an element of the new array, taking three arguments * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.map(user => user.tag); @@ -444,7 +439,6 @@ export class Collection extends Map { * * @param fn - Function that produces an element of the new collection, taking three arguments * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.mapValues(user => user.tag); @@ -466,7 +460,6 @@ export class Collection extends Map { * * @param fn - Function used to test (should return a boolean) * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.some(user => user.discriminator === '0000'); @@ -480,6 +473,7 @@ export class Collection extends Map { for (const [key, val] of this) { if (fn(val, key, this)) return true; } + return false; } @@ -489,7 +483,6 @@ export class Collection extends Map { * * @param fn - Function used to test (should return a boolean) * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection.every(user => !user.bot); @@ -513,6 +506,7 @@ export class Collection extends Map { for (const [key, val] of this) { if (!fn(val, key, this)) return false; } + return true; } @@ -523,7 +517,6 @@ export class Collection extends Map { * @param fn - Function used to reduce, taking four arguments; `accumulator`, `currentValue`, `currentKey`, * and `collection` * @param initialValue - Starting value for the accumulator - * * @example * ```ts * collection.reduce((acc, guild) => acc + guild.memberCount, 0); @@ -538,6 +531,7 @@ export class Collection extends Map { for (const [key, val] of this) accumulator = fn(accumulator, val, key, this); return accumulator; } + let first = true; for (const [key, val] of this) { if (first) { @@ -545,6 +539,7 @@ export class Collection extends Map { first = false; continue; } + accumulator = fn(accumulator, val, key, this); } @@ -563,7 +558,6 @@ export class Collection extends Map { * * @param fn - Function to execute for each element * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection @@ -576,6 +570,7 @@ export class Collection extends Map { public each(fn: (this: T, value: V, key: K, collection: this) => void, thisArg: T): this; public each(fn: (value: V, key: K, collection: this) => void, thisArg?: unknown): this { if (typeof fn !== 'function') throw new TypeError(`${fn} is not a function`); + // eslint-disable-next-line unicorn/no-array-method-this-argument this.forEach(fn as (value: V, key: K, map: Map) => void, thisArg); return this; } @@ -585,7 +580,6 @@ export class Collection extends Map { * * @param fn - Function to execute * @param thisArg - Value to use as `this` when executing function - * * @example * ```ts * collection @@ -619,7 +613,6 @@ export class Collection extends Map { * Combines this collection with others into a new collection. None of the source collections are modified. * * @param collections - Collections to merge - * * @example * ```ts * const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl); @@ -630,6 +623,7 @@ export class Collection extends Map { for (const coll of collections) { for (const [key, val] of coll) newColl.set(key, val); } + return newColl; } @@ -639,7 +633,6 @@ export class Collection extends Map { * the collections may be different objects, but contain the same data. * * @param collection - Collection to compare with - * * @returns Whether the collections have identical contents */ public equals(collection: ReadonlyCollection) { @@ -652,6 +645,7 @@ export class Collection extends Map { return false; } } + return true; } @@ -662,7 +656,6 @@ export class Collection extends Map { * * @param compareFunction - Specifies a function that defines the sort order. * If omitted, the collection is sorted according to each character's Unicode code point value, according to the string conversion of each element. - * * @example * ```ts * collection.sort((userA, userB) => userA.createdTimestamp - userB.createdTimestamp); @@ -679,6 +672,7 @@ export class Collection extends Map { for (const [k, v] of entries) { super.set(k, v); } + return this; } @@ -694,6 +688,7 @@ export class Collection extends Map { coll.set(k, v); } } + return coll; } @@ -702,24 +697,26 @@ export class Collection extends Map { * * @param other - The other Collection to filter against */ - public difference(other: ReadonlyCollection): Collection { - const coll = new this.constructor[Symbol.species](); + public difference(other: ReadonlyCollection): Collection { + const coll = new this.constructor[Symbol.species](); for (const [k, v] of other) { if (!this.has(k)) coll.set(k, v); } + for (const [k, v] of this) { if (!other.has(k)) coll.set(k, v); } + return coll; } /** * Merges two Collections together into a new Collection. + * * @param other - The other Collection to merge with * @param whenInSelf - Function getting the result if the entry only exists in this Collection * @param whenInOther - Function getting the result if the entry only exists in the other Collection * @param whenInBoth - Function getting the result if the entry exists in both Collections - * * @example * ```ts * // Sums up the entries in two collections. @@ -730,7 +727,6 @@ export class Collection extends Map { * (x, y) => ({ keep: true, value: x + y }), * ); * ``` - * * @example * ```ts * // Intersects two collections in a left-biased manner. @@ -765,6 +761,7 @@ export class Collection extends Map { if (r.keep) coll.set(k, r.value); } } + return coll; } @@ -776,7 +773,6 @@ export class Collection extends Map { * @param compareFunction - Specifies a function that defines the sort order. * If omitted, the collection is sorted according to each character's Unicode code point value, * according to the string conversion of each element. - * * @example * ```ts * collection.sorted((userA, userB) => userA.createdTimestamp - userB.createdTimestamp); @@ -800,7 +796,6 @@ export class Collection extends Map { * * @param entries - The list of entries * @param combine - Function to combine an existing entry with a new one - * * @example * ```ts * Collection.combineEntries([["a", 1], ["b", 2], ["a", 2]], (x, y) => x + y); @@ -819,6 +814,7 @@ export class Collection extends Map { coll.set(k, v); } } + return coll; } } @@ -826,7 +822,7 @@ export class Collection extends Map { /** * @internal */ -export type Keep = { keep: true; value: V } | { keep: false }; +export type Keep = { keep: false } | { keep: true; value: V }; /** * @internal diff --git a/packages/collection/src/index.ts b/packages/collection/src/index.ts index 989b92da864d..da5b1e4674fc 100644 --- a/packages/collection/src/index.ts +++ b/packages/collection/src/index.ts @@ -1 +1 @@ -export * from './collection'; +export * from './collection.js'; diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 615c0fc3d7ac..f61813daa84b 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -51,13 +51,8 @@ "@favware/cliff-jumper": "^1.8.7", "@types/jsdoc-to-markdown": "^7.0.3", "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/docgen/src/cli.ts b/packages/docgen/src/cli.ts index 0e00e33f19d9..9ecfeed3b519 100644 --- a/packages/docgen/src/cli.ts +++ b/packages/docgen/src/cli.ts @@ -1,13 +1,15 @@ #!/usr/bin/env node +/* eslint-disable n/shebang */ +import process from 'node:process'; import { createCommand } from 'commander'; -import { build } from './index.js'; import packageFile from '../package.json'; +import { build } from './index.js'; export interface CLIOptions { - input: string[]; custom: string; - root: string; + input: string[]; output: string; + root: string; typescript: boolean; } diff --git a/packages/docgen/src/documentation.ts b/packages/docgen/src/documentation.ts index fbfeaf41c46e..7b4830064062 100644 --- a/packages/docgen/src/documentation.ts +++ b/packages/docgen/src/documentation.ts @@ -1,5 +1,6 @@ import { dirname, join, relative } from 'node:path'; import type { DeclarationReflection } from 'typedoc'; +import packageFile from '../package.json'; import type { ChildTypes, Class, Config, CustomDocs, RootTypes } from './interfaces/index.js'; import { DocumentedClass } from './types/class.js'; import { DocumentedConstructor } from './types/constructor.js'; @@ -9,7 +10,6 @@ import { DocumentedInterface } from './types/interface.js'; import { DocumentedMember } from './types/member.js'; import { DocumentedMethod } from './types/method.js'; import { DocumentedTypeDef } from './types/typedef.js'; -import packageFile from '../package.json'; export class Documentation { public readonly classes = new Map(); @@ -23,7 +23,7 @@ export class Documentation { public readonly externals = new Map(); public constructor( - data: RootTypes[] | DeclarationReflection[], + data: DeclarationReflection[] | RootTypes[], private readonly config: Config, private readonly custom?: Record, ) { @@ -37,6 +37,7 @@ export class Documentation { if (item.children) { this.parse(item.children, item); } + break; } @@ -52,6 +53,7 @@ export class Documentation { if (item.children) { this.parse(item.children, item); } + break; default: @@ -60,37 +62,43 @@ export class Documentation { } } else { let items = data as RootTypes[]; - items = items.filter((i) => !i.ignore); + items = items.filter((item) => !item.ignore); for (const item of items) { switch (item.kind) { case 'class': { this.classes.set(item.name, new DocumentedClass(item, config)); - items = items.filter((i) => i.longname !== item.longname || i.kind !== item.kind); + items = items.filter((otherItem) => otherItem.longname !== item.longname || otherItem.kind !== item.kind); break; } + case 'function': { if (item.scope === 'global' || !item.memberof) { this.functions.set(item.name, new DocumentedMethod(item, config)); - items = items.filter((i) => i.longname !== item.longname); + items = items.filter((otherItem) => otherItem.longname !== item.longname); } + break; } + case 'interface': { this.interfaces.set(item.name, new DocumentedInterface(item as unknown as Class, config)); - items = items.filter((i) => i.longname !== item.longname); + items = items.filter((otherItem) => otherItem.longname !== item.longname); break; } + case 'typedef': { this.typedefs.set(item.name, new DocumentedTypeDef(item, config)); - items = items.filter((i) => i.longname !== item.longname); + items = items.filter((otherItem) => otherItem.longname !== item.longname); break; } + case 'external': { this.externals.set(item.name, new DocumentedExternal(item, config)); - items = items.filter((i) => i.longname !== item.longname); + items = items.filter((otherItem) => otherItem.longname !== item.longname); break; } + default: break; } @@ -100,39 +108,43 @@ export class Documentation { } } - public parse(items: ChildTypes[] | DeclarationReflection[], p?: DeclarationReflection) { + public parse(items: ChildTypes[] | DeclarationReflection[], prop?: DeclarationReflection) { if (this.config.typescript) { const it = items as DeclarationReflection[]; for (const member of it) { - let item: DocumentedMethod | DocumentedConstructor | DocumentedMember | DocumentedEvent | null = null; + let item: DocumentedConstructor | DocumentedEvent | DocumentedMember | DocumentedMethod | null = null; switch (member.kindString) { case 'Constructor': { item = new DocumentedConstructor(member, this.config); break; } + case 'Method': { - const event = p?.groups?.find((group) => group.title === 'Events'); + const event = prop?.groups?.find((group) => group.title === 'Events'); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if ((event?.children as unknown as number[])?.includes(member.id)) { item = new DocumentedEvent(member, this.config); break; } + item = new DocumentedMethod(member, this.config); break; } + case 'Property': { item = new DocumentedMember(member, this.config); break; } + default: { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions console.warn(`- Unknown documentation kind "${member.kindString}" - \n${JSON.stringify(member)}\n`); } } - const parent = this.classes.get(p!.name) ?? this.interfaces.get(p!.name); + const parent = this.classes.get(prop!.name) ?? this.interfaces.get(prop!.name); if (parent) { if (item) { parent.add(item); @@ -141,6 +153,7 @@ export class Documentation { `- Documentation item could not be constructed for "${member.name}" - \n${JSON.stringify(member)}\n`, ); } + continue; } @@ -155,9 +168,10 @@ export class Documentation { path: dirname(member.sources?.[0]?.fileName ?? ''), }; - if (p!.name) { - info.push(`member of "${p!.name}"`); + if (prop!.name) { + info.push(`member of "${prop!.name}"`); } + if (meta) { info.push( `${relative(this.config.root, join(meta.path, meta.file ?? ''))}${meta.line ? `:${meta.line}` : ''}`, @@ -173,27 +187,31 @@ export class Documentation { const it = items as ChildTypes[]; for (const member of it) { - let item: DocumentedMethod | DocumentedConstructor | DocumentedMember | DocumentedEvent | null = null; + let item: DocumentedConstructor | DocumentedEvent | DocumentedMember | DocumentedMethod | null = null; switch (member.kind) { case 'constructor': { item = new DocumentedConstructor(member, this.config); break; } + case 'function': { item = new DocumentedMethod(member, this.config); break; } + case 'member': { item = new DocumentedMember(member, this.config); break; } + case 'event': { item = new DocumentedEvent(member, this.config); break; } + default: { - // @ts-expect-error + // @ts-expect-error: This is a valid case // eslint-disable-next-line @typescript-eslint/restrict-template-expressions console.warn(`- Unknown documentation kind "${member.kind}" - \n${JSON.stringify(member)}\n`); } @@ -208,12 +226,13 @@ export class Documentation { `- Documentation item could not be constructed for "${member.name}" - \n${JSON.stringify(member)}\n`, ); } + continue; } const info = []; const name = (member.name || item?.data.name) ?? 'UNKNOWN'; - // @ts-expect-error + // @ts-expect-error: Typescript can't infer this // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-condition const memberof = member.memberof ?? item?.data?.memberof; const meta = @@ -224,6 +243,7 @@ export class Documentation { if (memberof) { info.push(`member of "${memberof as string}"`); } + if (meta) { info.push(`${relative(this.config.root, join(meta.path, meta.file))}${meta.line ? `:${meta.line}` : ''}`); } @@ -243,16 +263,14 @@ export class Documentation { format: Documentation.FORMAT_VERSION, date: Date.now(), }, - classes: [...this.classes.values()].map((c) => c.serialize()), - functions: [...this.functions.values()].map((f) => f.serialize()), - interfaces: [...this.interfaces.values()].map((i) => i.serialize()), - typedefs: [...this.typedefs.values()].map((t) => t.serialize()), - externals: [...this.externals.values()].map((e) => e.serialize()), + classes: [...this.classes.values()].map((_class) => _class.serialize()), + functions: [...this.functions.values()].map((_function) => _function.serialize()), + interfaces: [...this.interfaces.values()].map((_interface) => _interface.serialize()), + typedefs: [...this.typedefs.values()].map((_typedef) => _typedef.serialize()), + externals: [...this.externals.values()].map((_external) => _external.serialize()), custom: this.custom, }; } - public static get FORMAT_VERSION() { - return 30; - } + public static readonly FORMAT_VERSION = 30; } diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts index 195c4b88045f..de35a5009f7f 100644 --- a/packages/docgen/src/index.ts +++ b/packages/docgen/src/index.ts @@ -3,22 +3,22 @@ import { dirname, join, extname, basename, relative } from 'node:path'; import jsdoc2md from 'jsdoc-to-markdown'; import { type DeclarationReflection, Application, TSConfigReader } from 'typedoc'; import type { CLIOptions } from './cli'; -import { Documentation } from './documentation'; +import { Documentation } from './documentation.js'; import type { RootTypes, ChildTypes, CustomDocs } from './interfaces'; interface CustomFiles { - id?: string; - name: string; - path?: string; files: { id?: string; name: string; path: string; }[]; + id?: string; + name: string; + path?: string; } export function build({ input, custom: customDocs, root, output, typescript }: CLIOptions) { - let data: (RootTypes & ChildTypes)[] | DeclarationReflection[] = []; + let data: (ChildTypes & RootTypes)[] | DeclarationReflection[] = []; if (typescript) { console.log('Parsing Typescript in source files...'); const app = new Application(); @@ -26,13 +26,14 @@ export function build({ input, custom: customDocs, root, output, typescript }: C app.bootstrap({ entryPoints: input }); const project = app.convert(); if (project) { - // @ts-expect-error + // @ts-expect-error: Types are lost with this method data = app.serializer.toObject(project).children!; console.log(`${data.length} items parsed.`); } } else { console.log('Parsing JSDocs in source files...'); - data = jsdoc2md.getTemplateDataSync({ files: input }) as (RootTypes & ChildTypes)[]; + // eslint-disable-next-line n/no-sync + data = jsdoc2md.getTemplateDataSync({ files: input }) as (ChildTypes & RootTypes)[]; console.log(`${data.length} JSDoc items parsed.`); } @@ -40,7 +41,7 @@ export function build({ input, custom: customDocs, root, output, typescript }: C if (customDocs) { console.log('Loading custom docs files...'); const customDir = dirname(customDocs); - const file = readFileSync(customDocs, 'utf-8'); + const file = readFileSync(customDocs, 'utf8'); const data = JSON.parse(file) as CustomFiles[]; for (const category of data) { @@ -51,23 +52,23 @@ export function build({ input, custom: customDocs, root, output, typescript }: C files: {}, }; - for (const f of category.files) { - const fileRootPath = join(dir, f.path); - const extension = extname(f.path); - const fileId = f.id ?? basename(f.path, extension); - const fileData = readFileSync(fileRootPath, 'utf-8'); + for (const file of category.files) { + const fileRootPath = join(dir, file.path); + const extension = extname(file.path); + const fileId = file.id ?? basename(file.path, extension); + const fileData = readFileSync(fileRootPath, 'utf8'); custom[categoryId]!.files[fileId] = { - name: f.name, + name: file.name, type: extension.toLowerCase().replace(/^\./, ''), content: fileData, - path: relative(root, fileRootPath).replace(/\\/g, '/'), + path: relative(root, fileRootPath).replaceAll('\\', '/'), }; } } const fileCount = Object.keys(custom) - .map((k) => Object.keys(custom[k]!)) - .reduce((prev, c) => prev + c.length, 0); + .map((key) => Object.keys(custom[key]!)) + .reduce((prev, content) => prev + content.length, 0); const categoryCount = Object.keys(custom).length; console.log( `${fileCount} custom docs file${fileCount === 1 ? '' : 's'} in ` + @@ -82,5 +83,6 @@ export function build({ input, custom: customDocs, root, output, typescript }: C console.log(`Writing to ${output}...`); writeFileSync(output, JSON.stringify(docs.serialize())); } + console.log('Done!'); } diff --git a/packages/docgen/src/interfaces/access.type.ts b/packages/docgen/src/interfaces/access.type.ts index 29c4a4be05ef..f60aa6df5382 100644 --- a/packages/docgen/src/interfaces/access.type.ts +++ b/packages/docgen/src/interfaces/access.type.ts @@ -1 +1 @@ -export type Access = 'public' | 'private' | 'protected'; +export type Access = 'private' | 'protected' | 'public'; diff --git a/packages/docgen/src/interfaces/childTypes.type.ts b/packages/docgen/src/interfaces/childTypes.type.ts index 7c1181871bad..0f74d224f1cd 100644 --- a/packages/docgen/src/interfaces/childTypes.type.ts +++ b/packages/docgen/src/interfaces/childTypes.type.ts @@ -1,3 +1,3 @@ import type { Constructor, Event, Member, Method } from './index.js'; -export type ChildTypes = Constructor | Member | Method | Event; +export type ChildTypes = Constructor | Event | Member | Method; diff --git a/packages/docgen/src/interfaces/class.interface.ts b/packages/docgen/src/interfaces/class.interface.ts index 7ef86fe58ad6..278f85f347cc 100644 --- a/packages/docgen/src/interfaces/class.interface.ts +++ b/packages/docgen/src/interfaces/class.interface.ts @@ -1,13 +1,13 @@ import type { Access, Item, Meta, Scope } from './index.js'; export interface Class extends Item { + access?: Access; + augments?: string[]; + deprecated?: boolean | string; + implements?: string[]; kind: 'class'; + meta: Meta; scope: Scope; - implements?: string[]; - augments?: string[]; see?: string[]; - access?: Access; virtual?: boolean; - deprecated?: boolean | string; - meta: Meta; } diff --git a/packages/docgen/src/interfaces/config.interface.ts b/packages/docgen/src/interfaces/config.interface.ts index 3a72f94d1610..f73ebf8b48d5 100644 --- a/packages/docgen/src/interfaces/config.interface.ts +++ b/packages/docgen/src/interfaces/config.interface.ts @@ -1,7 +1,7 @@ export interface Config { - input: string[]; custom: string; - root: string; + input: string[]; output: string; + root: string; typescript: boolean; } diff --git a/packages/docgen/src/interfaces/constructor.interface.ts b/packages/docgen/src/interfaces/constructor.interface.ts index 2e18838b03da..da029d23d0f5 100644 --- a/packages/docgen/src/interfaces/constructor.interface.ts +++ b/packages/docgen/src/interfaces/constructor.interface.ts @@ -1,9 +1,9 @@ import type { Access, Item, Param } from './index.js'; export interface Constructor extends Item { + access?: Access; kind: 'constructor'; memberof: string; - see?: string[]; - access?: Access; params?: Param[]; + see?: string[]; } diff --git a/packages/docgen/src/interfaces/customDocs.interface.ts b/packages/docgen/src/interfaces/customDocs.interface.ts index 5ad119c8b9be..87b92a5cc266 100644 --- a/packages/docgen/src/interfaces/customDocs.interface.ts +++ b/packages/docgen/src/interfaces/customDocs.interface.ts @@ -1,12 +1,12 @@ export interface CustomDocs { - name?: string; files: Record< string, { - name?: string; - type?: string; content?: string; + name?: string; path?: string; + type?: string; } >; + name?: string; } diff --git a/packages/docgen/src/interfaces/event.interface.ts b/packages/docgen/src/interfaces/event.interface.ts index fc389b5c31fd..b0407af8e750 100644 --- a/packages/docgen/src/interfaces/event.interface.ts +++ b/packages/docgen/src/interfaces/event.interface.ts @@ -1,11 +1,11 @@ import type { Item, Meta, Param, Scope } from './index.js'; export interface Event extends Item { + deprecated?: boolean | string; kind: 'event'; - scope: Scope; memberof: string; - see?: string[]; - deprecated?: boolean | string; - params?: Param[]; meta: Meta; + params?: Param[]; + scope: Scope; + see?: string[]; } diff --git a/packages/docgen/src/interfaces/exception.interface.ts b/packages/docgen/src/interfaces/exception.interface.ts index 763bb541fadc..7de11b1ce7d0 100644 --- a/packages/docgen/src/interfaces/exception.interface.ts +++ b/packages/docgen/src/interfaces/exception.interface.ts @@ -1,7 +1,7 @@ import type { Type } from './index.js'; export interface Exception { - type: Type; - nullable?: boolean; description?: string; + nullable?: boolean; + type: Type; } diff --git a/packages/docgen/src/interfaces/external.interface.ts b/packages/docgen/src/interfaces/external.interface.ts index aef5da7fd7cb..e404dc215ed3 100644 --- a/packages/docgen/src/interfaces/external.interface.ts +++ b/packages/docgen/src/interfaces/external.interface.ts @@ -2,6 +2,6 @@ import type { Item, Meta } from './index.js'; export interface External extends Item { kind: 'external'; - see?: string[]; meta: Meta; + see?: string[]; } diff --git a/packages/docgen/src/interfaces/interface.interface.ts b/packages/docgen/src/interfaces/interface.interface.ts index 01aa7293e65d..3236760b5d03 100644 --- a/packages/docgen/src/interfaces/interface.interface.ts +++ b/packages/docgen/src/interfaces/interface.interface.ts @@ -1,7 +1,7 @@ import type { Class } from './index.js'; -// @ts-expect-error +// @ts-expect-error: Inheritance type error export interface Interface extends Class { - kind: 'interface'; classdesc: string; + kind: 'interface'; } diff --git a/packages/docgen/src/interfaces/item.interface.ts b/packages/docgen/src/interfaces/item.interface.ts index a160b3128d1f..0c3bdbe13d74 100644 --- a/packages/docgen/src/interfaces/item.interface.ts +++ b/packages/docgen/src/interfaces/item.interface.ts @@ -1,9 +1,9 @@ export interface Item { + description: string; id: string; + ignore?: boolean; + kind: string; longname: string; name: string; - kind: string; - description: string; order: number; - ignore?: boolean; } diff --git a/packages/docgen/src/interfaces/member.interface.ts b/packages/docgen/src/interfaces/member.interface.ts index c71a08379924..0c6430b6cec6 100644 --- a/packages/docgen/src/interfaces/member.interface.ts +++ b/packages/docgen/src/interfaces/member.interface.ts @@ -1,17 +1,17 @@ import type { Access, Item, Meta, Param, Scope, Type } from './index.js'; export interface Member extends Item { + access?: Access; + default?: string; + deprecated?: boolean | string; kind: 'member'; - see?: string[]; - scope: Scope; memberof: string; - type: Type; - access?: Access; - readonly?: boolean; + meta: Meta; nullable?: boolean; - virtual?: boolean; - deprecated?: boolean | string; - default?: string; properties?: Param[]; - meta: Meta; + readonly?: boolean; + scope: Scope; + see?: string[]; + type: Type; + virtual?: boolean; } diff --git a/packages/docgen/src/interfaces/meta.interface.ts b/packages/docgen/src/interfaces/meta.interface.ts index df50bf525991..d1a4bfc738bd 100644 --- a/packages/docgen/src/interfaces/meta.interface.ts +++ b/packages/docgen/src/interfaces/meta.interface.ts @@ -1,5 +1,5 @@ export interface Meta { - lineno: number; filename: string; + lineno: number; path: string; } diff --git a/packages/docgen/src/interfaces/method.interface.ts b/packages/docgen/src/interfaces/method.interface.ts index 4d2421d208ad..737e7f584484 100644 --- a/packages/docgen/src/interfaces/method.interface.ts +++ b/packages/docgen/src/interfaces/method.interface.ts @@ -1,22 +1,22 @@ import type { Access, Exception, Item, Meta, Param, Return, Scope } from './index.js'; export interface Method extends Item { - kind: 'function'; - see?: string[]; - scope: Scope; access?: Access; - inherits?: string; - inherited?: boolean; - implements?: string[]; - examples?: string[]; - virtual?: boolean; + async?: boolean; deprecated?: boolean | string; + examples?: string[]; + exceptions?: Exception[]; + fires?: string[]; + generator?: boolean; + implements?: string[]; + inherited?: boolean; + inherits?: string; + kind: 'function'; memberof?: string; + meta: Meta; params?: Param[]; - async?: boolean; - generator?: boolean; - fires?: string[]; returns?: Return[]; - exceptions?: Exception[]; - meta: Meta; + scope: Scope; + see?: string[]; + virtual?: boolean; } diff --git a/packages/docgen/src/interfaces/param.interface.ts b/packages/docgen/src/interfaces/param.interface.ts index d6bde6839533..ee3a8b7a879b 100644 --- a/packages/docgen/src/interfaces/param.interface.ts +++ b/packages/docgen/src/interfaces/param.interface.ts @@ -1,11 +1,11 @@ import type { Type } from './index.js'; export interface Param { - type: Type; + defaultvalue?: string; description: string; name: string; + nullable?: boolean; optional?: boolean; - defaultvalue?: string; + type: Type; variable?: string; - nullable?: boolean; } diff --git a/packages/docgen/src/interfaces/return.interface.ts b/packages/docgen/src/interfaces/return.interface.ts index b567eb4104a4..3dd0d1e1b673 100644 --- a/packages/docgen/src/interfaces/return.interface.ts +++ b/packages/docgen/src/interfaces/return.interface.ts @@ -1,7 +1,7 @@ import type { Type } from './index.js'; export interface Return { - type: Required; - nullable?: boolean; description?: string; + nullable?: boolean; + type: Required; } diff --git a/packages/docgen/src/interfaces/rootTypes.type.ts b/packages/docgen/src/interfaces/rootTypes.type.ts index 0247aa03e42b..a6352a309647 100644 --- a/packages/docgen/src/interfaces/rootTypes.type.ts +++ b/packages/docgen/src/interfaces/rootTypes.type.ts @@ -1,3 +1,3 @@ import type { Class, External, Interface, Method, Typedef } from './index.js'; -export type RootTypes = Class | Method | Interface | Typedef | External; +export type RootTypes = Class | External | Interface | Method | Typedef; diff --git a/packages/docgen/src/interfaces/typedef.interface.ts b/packages/docgen/src/interfaces/typedef.interface.ts index 73da28bbec13..9d86a476fc6c 100644 --- a/packages/docgen/src/interfaces/typedef.interface.ts +++ b/packages/docgen/src/interfaces/typedef.interface.ts @@ -1,14 +1,14 @@ import type { Access, Item, Meta, Param, Return, Scope, Type } from './index.js'; export interface Typedef extends Item { - kind: 'typedef'; - scope: Scope; - see?: string[]; access?: Access; deprecated?: boolean | string; - type: Type; - properties?: Param[]; + kind: 'typedef'; + meta: Meta; params?: Param[]; + properties?: Param[]; returns?: Return[]; - meta: Meta; + scope: Scope; + see?: string[]; + type: Type; } diff --git a/packages/docgen/src/interfaces/var-type.interface.ts b/packages/docgen/src/interfaces/var-type.interface.ts index 990f11267814..cef3abe92ff7 100644 --- a/packages/docgen/src/interfaces/var-type.interface.ts +++ b/packages/docgen/src/interfaces/var-type.interface.ts @@ -1,7 +1,7 @@ import type { Type } from './index.js'; export interface VarType extends Type { - type?: Required | undefined; description?: string | undefined; nullable?: boolean | undefined; + type?: Required | undefined; } diff --git a/packages/docgen/src/types/class.ts b/packages/docgen/src/types/class.ts index 581ff8336106..0a2aa0eb8e58 100644 --- a/packages/docgen/src/types/class.ts +++ b/packages/docgen/src/types/class.ts @@ -1,5 +1,7 @@ import { parse } from 'node:path'; import type { DeclarationReflection } from 'typedoc'; +import type { Class, Config } from '../interfaces/index.js'; +import { parseType } from '../util/parseType.js'; import { DocumentedConstructor } from './constructor.js'; import { DocumentedEvent } from './event.js'; import { DocumentedItemMeta } from './item-meta.js'; @@ -7,8 +9,6 @@ import { DocumentedItem } from './item.js'; import { DocumentedMember } from './member.js'; import { DocumentedMethod } from './method.js'; import { DocumentedVarType } from './var-type.js'; -import type { Class, Config } from '../interfaces/index.js'; -import { parseType } from '../util/parseType.js'; export class DocumentedClass extends DocumentedItem { public readonly props = new Map(); @@ -27,51 +27,55 @@ export class DocumentedClass extends DocumentedItem t.tag === '@see').length + const see = signature.comment?.blockTags?.filter((block) => block.tag === '@see').length ? signature.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((contentText) => contentText.kind === 'text')?.text.trim()) : undefined; return { - // @ts-expect-error + // @ts-expect-error: Type cannot be inferred // eslint-disable-next-line @typescript-eslint/no-unsafe-argument name: signature.name === 'default' ? parse(meta?.file ?? 'default').name : signature.name, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: signature.comment?.summary.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, extends: this.extends?.serialize(), @@ -106,22 +110,23 @@ export class DocumentedClass extends DocumentedItem t.tag === '@private' || t.tag === '@internal') + signature.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/no-unnecessary-condition - abstract: signature.comment?.blockTags?.some((t) => t.tag === '@abstract') || undefined, + abstract: signature.comment?.blockTags?.some((block) => block.tag === '@abstract') || undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - deprecated: signature.comment?.blockTags?.some((t) => t.tag === '@deprecated') + deprecated: signature.comment?.blockTags?.some((block) => block.tag === '@deprecated') ? signature.comment.blockTags - .find((t) => t.tag === '@deprecated') + .find((block) => block.tag === '@deprecated') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim() ?? true : undefined, construct: this.construct?.serialize(), - props: this.props.size ? [...this.props.values()].map((p) => p.serialize()) : undefined, - methods: this.methods.size ? [...this.methods.values()].map((m) => m.serialize()) : undefined, - events: this.events.size ? [...this.events.values()].map((e) => e.serialize()) : undefined, + props: this.props.size ? [...this.props.values()].map((param) => param.serialize()) : undefined, + methods: this.methods.size ? [...this.methods.values()].map((method) => method.serialize()) : undefined, + events: this.events.size ? [...this.events.values()].map((event) => event.serialize()) : undefined, meta, }; } @@ -137,9 +142,9 @@ export class DocumentedClass extends DocumentedItem p.serialize()) : undefined, - methods: this.methods.size ? [...this.methods.values()].map((m) => m.serialize()) : undefined, - events: this.events.size ? [...this.events.values()].map((e) => e.serialize()) : undefined, + props: this.props.size ? [...this.props.values()].map((param) => param.serialize()) : undefined, + methods: this.methods.size ? [...this.methods.values()].map((method) => method.serialize()) : undefined, + events: this.events.size ? [...this.events.values()].map((event) => event.serialize()) : undefined, meta: new DocumentedItemMeta(data.meta, this.config).serialize(), }; } diff --git a/packages/docgen/src/types/constructor.ts b/packages/docgen/src/types/constructor.ts index 80397be26012..c88ab68b1717 100644 --- a/packages/docgen/src/types/constructor.ts +++ b/packages/docgen/src/types/constructor.ts @@ -1,7 +1,7 @@ import type { DeclarationReflection, SignatureReflection } from 'typedoc'; +import type { Constructor } from '../interfaces/index.js'; import { DocumentedItem } from './item.js'; import { DocumentedParam } from './param.js'; -import type { Constructor } from '../interfaces/index.js'; export class DocumentedConstructor extends DocumentedItem { public override serializer() { @@ -10,26 +10,28 @@ export class DocumentedConstructor extends DocumentedItem t.tag === '@see').length + const see = signature.comment?.blockTags?.filter((block) => block.tag === '@see').length ? signature.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((textContent) => textContent.kind === 'text')?.text.trim()) : undefined; return { name: signature.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: signature.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, access: data.flags.isPrivate || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - signature.comment?.blockTags?.some((t) => t.tag === '@private' || t.tag === '@internal') + signature.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, - // @ts-expect-error + // @ts-expect-error: No type for params params: signature.parameters - ? (signature as SignatureReflection).parameters?.map((p) => new DocumentedParam(p, this.config).serialize()) + ? (signature as SignatureReflection).parameters?.map((param) => + new DocumentedParam(param, this.config).serialize(), + ) : undefined, }; } @@ -40,7 +42,9 @@ export class DocumentedConstructor extends DocumentedItem new DocumentedParam(p, this.config).serialize()) : undefined, + params: data.params?.length + ? data.params.map((param) => new DocumentedParam(param, this.config).serialize()) + : undefined, }; } } diff --git a/packages/docgen/src/types/event.ts b/packages/docgen/src/types/event.ts index 0158b7ebb6b6..6af0b050ee02 100644 --- a/packages/docgen/src/types/event.ts +++ b/packages/docgen/src/types/event.ts @@ -1,12 +1,12 @@ import type { DeclarationReflection, SignatureReflection } from 'typedoc'; +import type { Event } from '../interfaces/index.js'; +import { parseType } from '../util/parseType.js'; import { DocumentedItemMeta } from './item-meta.js'; import { DocumentedItem } from './item.js'; import { DocumentedParam } from './param.js'; import { DocumentedVarType } from './var-type.js'; -import type { Event } from '../interfaces/index.js'; -import { parseType } from '../util/parseType.js'; -export class DocumentedEvent extends DocumentedItem { +export class DocumentedEvent extends DocumentedItem { public override serializer() { if (this.config.typescript) { const data = this.data as DeclarationReflection; @@ -19,45 +19,47 @@ export class DocumentedEvent extends DocumentedItem t.tag === '@see').length + const see = signature.comment?.blockTags?.filter((block) => block.tag === '@see').length ? signature.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((contentText) => contentText.kind === 'text')?.text.trim()) : undefined; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const examples = signature.comment?.blockTags?.filter((t) => t.tag === '@example').length + const examples = signature.comment?.blockTags?.filter((block) => block.tag === '@example').length ? signature.comment.blockTags - .filter((t) => t.tag === '@example') - .map((t) => t.content.reduce((prev, curr) => (prev += curr.text), '').trim()) + .filter((block) => block.tag === '@example') + // eslint-disable-next-line no-param-reassign + .map((block) => block.content.reduce((prev, curr) => (prev += curr.text), '').trim()) : undefined; return { - // @ts-expect-error + // @ts-expect-error: No type for params // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access name: signature.parameters?.[0]?.type?.value, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: signature.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, access: data.flags.isPrivate || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - signature.comment?.blockTags?.some((t) => t.tag === '@private' || t.tag === '@internal') + signature.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, examples, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - deprecated: signature.comment?.blockTags?.some((t) => t.tag === '@deprecated') + deprecated: signature.comment?.blockTags?.some((block) => block.tag === '@deprecated') ? signature.comment.blockTags - .find((t) => t.tag === '@deprecated') + .find((block) => block.tag === '@deprecated') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim() ?? true : undefined, - // @ts-expect-error + // @ts-expect-error: Parameters type is not available params: signature.parameters ? (signature as SignatureReflection).parameters ?.slice(1) - .map((p) => new DocumentedParam(p, this.config).serialize()) + .map((param) => new DocumentedParam(param, this.config).serialize()) : undefined, returns: signature.type ? [ @@ -67,7 +69,8 @@ export class DocumentedEvent extends DocumentedItem t.tag === '@returns') + ?.find((block) => block.tag === '@returns') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || undefined, @@ -79,7 +82,8 @@ export class DocumentedEvent extends DocumentedItem t.tag === '@returns') + ?.find((block) => block.tag === '@returns') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || undefined, @@ -93,7 +97,9 @@ export class DocumentedEvent extends DocumentedItem new DocumentedParam(p, this.config).serialize()) : undefined, + params: data.params?.length + ? data.params.map((param) => new DocumentedParam(param, this.config).serialize()) + : undefined, meta: new DocumentedItemMeta(data.meta, this.config).serialize(), }; } diff --git a/packages/docgen/src/types/external.ts b/packages/docgen/src/types/external.ts index 477797972d70..7b30a1b63304 100644 --- a/packages/docgen/src/types/external.ts +++ b/packages/docgen/src/types/external.ts @@ -1,6 +1,6 @@ +import type { External } from '../interfaces/index.js'; import { DocumentedItemMeta } from './item-meta.js'; import { DocumentedItem } from './item.js'; -import type { External } from '../interfaces/index.js'; export class DocumentedExternal extends DocumentedItem { public override serializer() { diff --git a/packages/docgen/src/types/interface.ts b/packages/docgen/src/types/interface.ts index 39b94f1d416e..f09cde53494a 100644 --- a/packages/docgen/src/types/interface.ts +++ b/packages/docgen/src/types/interface.ts @@ -1,5 +1,5 @@ -import { DocumentedClass } from './class.js'; import type { Interface } from '../interfaces/index.js'; +import { DocumentedClass } from './class.js'; export class DocumentedInterface extends DocumentedClass { public override serializer() { diff --git a/packages/docgen/src/types/item-meta.ts b/packages/docgen/src/types/item-meta.ts index 20d02719068b..e49ddde3c4b0 100644 --- a/packages/docgen/src/types/item-meta.ts +++ b/packages/docgen/src/types/item-meta.ts @@ -1,7 +1,7 @@ import { basename, relative } from 'node:path'; import type { SourceReference } from 'typedoc'; -import { DocumentedItem } from './item.js'; import type { Meta } from '../interfaces/index.js'; +import { DocumentedItem } from './item.js'; export class DocumentedItemMeta extends DocumentedItem { public override serializer() { @@ -20,7 +20,7 @@ export class DocumentedItemMeta extends DocumentedItem { return { line: data.lineno, file: data.filename, - path: relative(this.config.root, data.path).replace(/\\/g, '/'), + path: relative(this.config.root, data.path).replaceAll('\\', '/'), }; } } diff --git a/packages/docgen/src/types/item.ts b/packages/docgen/src/types/item.ts index 7128fe5a36dd..d8db85f239c3 100644 --- a/packages/docgen/src/types/item.ts +++ b/packages/docgen/src/types/item.ts @@ -1,14 +1,15 @@ import type { DeclarationReflection } from 'typedoc'; import type { Config, Item } from '../interfaces/index.js'; -export class DocumentedItem { +export class DocumentedItem { public constructor(public readonly data: T, public readonly config: Config) {} public serialize(): unknown { try { - return this.serializer(); - } catch (err) { - const error = err as Error; + this.serializer(); + return; + } catch (error_) { + const error = error_ as Error; error.message = `Error while serializing ${this.detailedName()}: ${error.message}`; throw error; } diff --git a/packages/docgen/src/types/member.ts b/packages/docgen/src/types/member.ts index ddd932b70519..d25dbf3a301e 100644 --- a/packages/docgen/src/types/member.ts +++ b/packages/docgen/src/types/member.ts @@ -1,12 +1,12 @@ import type { DeclarationReflection } from 'typedoc'; +import type { Member } from '../interfaces/index.js'; +import { parseType } from '../util/parseType.js'; import { DocumentedItemMeta } from './item-meta.js'; import { DocumentedItem } from './item.js'; import { DocumentedParam } from './param.js'; import { DocumentedVarType } from './var-type.js'; -import type { Member } from '../interfaces/index.js'; -import { parseType } from '../util/parseType.js'; -export class DocumentedMember extends DocumentedItem { +export class DocumentedMember extends DocumentedItem { public override serializer() { if (this.config.typescript) { const data = this.data as DeclarationReflection; @@ -19,31 +19,32 @@ export class DocumentedMember extends DocumentedItem t.tag === '@see').length + const see = signature.comment?.blockTags?.filter((block) => block.tag === '@see').length ? signature.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((contentText) => contentText.kind === 'text')?.text.trim()) : undefined; const base = { name: signature.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: signature.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, scope: data.flags.isStatic ? 'static' : undefined, access: data.flags.isPrivate || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - signature.comment?.blockTags?.some((t) => t.tag === '@private' || t.tag === '@internal') + signature.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, readonly: data.flags.isReadonly, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing - abstract: signature.comment?.blockTags?.some((t) => t.tag === '@abstract') || undefined, + abstract: signature.comment?.blockTags?.some((block) => block.tag === '@abstract') || undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - deprecated: signature.comment?.blockTags?.some((t) => t.tag === '@deprecated') + deprecated: signature.comment?.blockTags?.some((block) => block.tag === '@deprecated') ? signature.comment.blockTags - .find((t) => t.tag === '@deprecated') + .find((block) => block.tag === '@deprecated') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim() ?? true : undefined, @@ -51,7 +52,8 @@ export class DocumentedMember extends DocumentedItem t.tag === '@default') + ?.find((block) => block.tag === '@default') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || @@ -75,30 +77,31 @@ export class DocumentedMember extends DocumentedItem t.tag === '@see').length + const see = getter.comment?.blockTags?.filter((block) => block.tag === '@see').length ? getter.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((contentText) => contentText.kind === 'text')?.text.trim()) : undefined; return { ...base, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: getter.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, access: data.flags.isPrivate || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - getter.comment?.blockTags?.some((t) => t.tag === '@private' || t.tag === '@internal') + getter.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, readonly: base.readonly || !hasSetter, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing - abstract: getter.comment?.blockTags?.some((t) => t.tag === '@abstract') || undefined, + abstract: getter.comment?.blockTags?.some((block) => block.tag === '@abstract') || undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - deprecated: getter.comment?.blockTags?.some((t) => t.tag === '@deprecated') + deprecated: getter.comment?.blockTags?.some((block) => block.tag === '@deprecated') ? getter.comment.blockTags - .find((t) => t.tag === '@deprecated') + .find((block) => block.tag === '@deprecated') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim() ?? true : undefined, @@ -106,7 +109,8 @@ export class DocumentedMember extends DocumentedItem t.tag === '@default') + ?.find((block) => block.tag === '@default') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || @@ -132,7 +136,7 @@ export class DocumentedMember extends DocumentedItem new DocumentedParam(p, this.config).serialize()) + ? data.properties.map((prop) => new DocumentedParam(prop, this.config).serialize()) : undefined, meta: new DocumentedItemMeta(data.meta, this.config).serialize(), }; diff --git a/packages/docgen/src/types/method.ts b/packages/docgen/src/types/method.ts index dec8e6f7ebe7..cd20ab90c24a 100644 --- a/packages/docgen/src/types/method.ts +++ b/packages/docgen/src/types/method.ts @@ -1,12 +1,12 @@ import type { DeclarationReflection, SignatureReflection } from 'typedoc'; +import type { Method } from '../interfaces/index.js'; +import { parseType } from '../util/parseType.js'; import { DocumentedItemMeta } from './item-meta.js'; import { DocumentedItem } from './item.js'; import { DocumentedParam } from './param.js'; import { DocumentedVarType } from './var-type.js'; -import type { Method } from '../interfaces/index.js'; -import { parseType } from '../util/parseType.js'; -export class DocumentedMethod extends DocumentedItem { +export class DocumentedMethod extends DocumentedItem { public override serializer() { if (this.config.typescript) { const data = this.data as DeclarationReflection; @@ -19,46 +19,50 @@ export class DocumentedMethod extends DocumentedItem t.tag === '@see').length + const see = signature.comment?.blockTags?.filter((block) => block.tag === '@see').length ? signature.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((innerContent) => innerContent.kind === 'text')?.text.trim()) : undefined; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const examples = signature.comment?.blockTags?.filter((t) => t.tag === '@example').length + const examples = signature.comment?.blockTags?.filter((block) => block.tag === '@example').length ? signature.comment.blockTags - .filter((t) => t.tag === '@example') - .map((t) => t.content.reduce((prev, curr) => (prev += curr.text), '').trim()) + .filter((block) => block.tag === '@example') + // eslint-disable-next-line no-param-reassign + .map((block) => block.content.reduce((prev, curr) => (prev += curr.text), '').trim()) : undefined; return { name: signature.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: signature.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, scope: data.flags.isStatic ? 'static' : undefined, access: data.flags.isPrivate || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - signature.comment?.blockTags?.some((t) => t.tag === '@private' || t.tag === '@internal') + signature.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, examples, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing - abstract: signature.comment?.blockTags?.some((t) => t.tag === '@abstract') || undefined, + abstract: signature.comment?.blockTags?.some((block) => block.tag === '@abstract') || undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - deprecated: signature.comment?.blockTags?.some((t) => t.tag === '@deprecated') + deprecated: signature.comment?.blockTags?.some((block) => block.tag === '@deprecated') ? signature.comment.blockTags - .find((t) => t.tag === '@deprecated') + .find((block) => block.tag === '@deprecated') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim() ?? true : undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // emits: signature.comment?.blockTags?.filter((t) => t.tag === '@emits').map((t) => t.content), - // @ts-expect-error + // @ts-expect-error: Typescript doesn't know that this is a SignatureReflection params: signature.parameters - ? (signature as SignatureReflection).parameters?.map((p) => new DocumentedParam(p, this.config).serialize()) + ? (signature as SignatureReflection).parameters?.map((param) => + new DocumentedParam(param, this.config).serialize(), + ) : undefined, returns: signature.type ? [ @@ -68,7 +72,8 @@ export class DocumentedMethod extends DocumentedItem t.tag === '@returns') + ?.find((block) => block.tag === '@returns') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || undefined, @@ -80,7 +85,8 @@ export class DocumentedMethod extends DocumentedItem t.tag === '@returns') + ?.find((block) => block.tag === '@returns') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || undefined, @@ -103,13 +109,15 @@ export class DocumentedMethod extends DocumentedItem new DocumentedParam(p, this.config).serialize()) : undefined, + params: data.params?.length + ? data.params.map((param) => new DocumentedParam(param, this.config).serialize()) + : undefined, async: data.async, generator: data.generator, returns: data.returns?.length - ? data.returns.map((p) => + ? data.returns.map((param) => new DocumentedVarType( - { names: p.type.names, description: p.description, nullable: p.nullable }, + { names: param.type.names, description: param.description, nullable: param.nullable }, this.config, ).serialize(), ) diff --git a/packages/docgen/src/types/param.ts b/packages/docgen/src/types/param.ts index 89e428b6a6ab..9d4bfa36d3c3 100644 --- a/packages/docgen/src/types/param.ts +++ b/packages/docgen/src/types/param.ts @@ -1,8 +1,8 @@ import type { ParameterReflection } from 'typedoc'; -import { DocumentedItem } from './item.js'; -import { DocumentedVarType } from './var-type.js'; import type { Param } from '../interfaces/index.js'; import { parseType } from '../util/parseType.js'; +import { DocumentedItem } from './item.js'; +import { DocumentedVarType } from './var-type.js'; export class DocumentedParam extends DocumentedItem { public override serializer() { @@ -11,14 +11,15 @@ export class DocumentedParam extends DocumentedItem return { name: data.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: data.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, - optional: data.flags.isOptional || typeof data.defaultValue != 'undefined', + optional: data.flags.isOptional || typeof data.defaultValue !== 'undefined', default: (data.defaultValue === '...' ? undefined : data.defaultValue) ?? (data.comment?.blockTags // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ?.find((t) => t.tag === '@default') + ?.find((block) => block.tag === '@default') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || diff --git a/packages/docgen/src/types/typedef.ts b/packages/docgen/src/types/typedef.ts index 93bab4b2595f..17df02874473 100644 --- a/packages/docgen/src/types/typedef.ts +++ b/packages/docgen/src/types/typedef.ts @@ -1,13 +1,13 @@ import type { DeclarationReflection, LiteralType } from 'typedoc'; +import type { Typedef } from '../interfaces/index.js'; +import { parseType } from '../util/parseType.js'; +import { isReflectionType } from '../util/types.js'; import { DocumentedItemMeta } from './item-meta.js'; import { DocumentedItem } from './item.js'; import { DocumentedParam } from './param.js'; import { DocumentedVarType } from './var-type.js'; -import type { Typedef } from '../interfaces/index.js'; -import { parseType } from '../util/parseType.js'; -import { isReflectionType } from '../util/types.js'; -export class DocumentedTypeDef extends DocumentedItem { +export class DocumentedTypeDef extends DocumentedItem { public override serializer() { if (this.config.typescript) { const data = this.data as DeclarationReflection; @@ -20,27 +20,28 @@ export class DocumentedTypeDef extends DocumentedItem t.tag === '@see').length + const see = signature.comment?.blockTags?.filter((block) => block.tag === '@see').length ? signature.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((contentText) => contentText.kind === 'text')?.text.trim()) : undefined; const baseReturn = { name: signature.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: signature.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, access: data.flags.isPrivate || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - signature.comment?.blockTags?.some((t) => t.tag === '@private' || t.tag === '@internal') + signature.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - deprecated: signature.comment?.blockTags?.some((t) => t.tag === '@deprecated') + deprecated: signature.comment?.blockTags?.some((block) => block.tag === '@deprecated') ? signature.comment.blockTags - .find((t) => t.tag === '@deprecated') + .find((block) => block.tag === '@deprecated') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim() ?? true : undefined, @@ -63,7 +64,7 @@ export class DocumentedTypeDef extends DocumentedItem (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || undefined, @@ -81,19 +82,20 @@ export class DocumentedTypeDef extends DocumentedItem (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign child.signatures?.[0]?.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, - optional: child.flags.isOptional || typeof child.defaultValue != 'undefined', + optional: child.flags.isOptional || typeof child.defaultValue !== 'undefined', default: (child.defaultValue === '...' ? undefined : child.defaultValue) ?? (child.comment?.blockTags // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ?.find((t) => t.tag === '@default') + ?.find((block) => block.tag === '@default') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || @@ -111,7 +113,8 @@ export class DocumentedTypeDef extends DocumentedItem t.tag === '@returns') + ?.find((block) => block.tag === '@returns') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim(), }, @@ -131,14 +134,15 @@ export class DocumentedTypeDef extends DocumentedItem ({ name: param.name, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: param.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, - optional: param.flags.isOptional || typeof param.defaultValue != 'undefined', + optional: param.flags.isOptional || typeof param.defaultValue !== 'undefined', default: (param.defaultValue === '...' ? undefined : param.defaultValue) ?? (param.comment?.blockTags // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ?.find((t) => t.tag === '@default') + ?.find((block) => block.tag === '@default') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || @@ -149,27 +153,28 @@ export class DocumentedTypeDef extends DocumentedItem t.tag === '@see').length + const see = sig?.comment?.blockTags?.filter((block) => block.tag === '@see').length ? sig.comment.blockTags - .filter((t) => t.tag === '@see') - .map((t) => t.content.find((c) => c.kind === 'text')?.text.trim()) + .filter((block) => block.tag === '@see') + .map((block) => block.content.find((contentText) => contentText.kind === 'text')?.text.trim()) : undefined; return { ...baseReturn, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/prefer-nullish-coalescing, no-param-reassign description: sig?.comment?.summary?.reduce((prev, curr) => (prev += curr.text), '').trim() || undefined, see, access: sig?.flags.isPrivate || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - sig?.comment?.blockTags?.some((t) => t.tag === '@private' || t.tag === '@internal') + sig?.comment?.blockTags?.some((block) => block.tag === '@private' || block.tag === '@internal') ? 'private' : undefined, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - deprecated: sig?.comment?.blockTags?.some((t) => t.tag === '@deprecated') + deprecated: sig?.comment?.blockTags?.some((block) => block.tag === '@deprecated') ? sig.comment.blockTags - .find((t) => t.tag === '@deprecated') + .find((block) => block.tag === '@deprecated') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') .trim() ?? true : undefined, @@ -182,7 +187,8 @@ export class DocumentedTypeDef extends DocumentedItem t.tag === '@returns') + ?.find((block) => block.tag === '@returns') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || undefined, @@ -194,7 +200,8 @@ export class DocumentedTypeDef extends DocumentedItem t.tag === '@returns') + ?.find((block) => block.tag === '@returns') + // eslint-disable-next-line no-param-reassign ?.content.reduce((prev, curr) => (prev += curr.text), '') // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .trim() || undefined, @@ -215,11 +222,13 @@ export class DocumentedTypeDef extends DocumentedItem new DocumentedParam(p, this.config).serialize()) + ? data.properties.map((prop) => new DocumentedParam(prop, this.config).serialize()) + : undefined, + params: data.params?.length + ? data.params.map((param) => new DocumentedParam(param, this.config).serialize()) : undefined, - params: data.params?.length ? data.params.map((p) => new DocumentedParam(p, this.config).serialize()) : undefined, returns: data.returns?.length - ? data.returns.map((p) => new DocumentedVarType(p, this.config).serialize()) + ? data.returns.map((prop) => new DocumentedVarType(prop, this.config).serialize()) : undefined, meta: new DocumentedItemMeta(data.meta, this.config).serialize(), }; diff --git a/packages/docgen/src/types/var-type.ts b/packages/docgen/src/types/var-type.ts index d5636ba2a983..3fffe113f715 100644 --- a/packages/docgen/src/types/var-type.ts +++ b/packages/docgen/src/types/var-type.ts @@ -1,7 +1,7 @@ -import { DocumentedItem } from './item.js'; import type { VarType } from '../interfaces/index.js'; import { parseType } from '../util/parseType.js'; import { splitVarName } from '../util/splitVarName.js'; +import { DocumentedItem } from './item.js'; export class DocumentedVarType extends DocumentedItem { public override serializer() { diff --git a/packages/docgen/src/util/parseType.ts b/packages/docgen/src/util/parseType.ts index e75a5974ea76..06f7c17b5e74 100644 --- a/packages/docgen/src/util/parseType.ts +++ b/packages/docgen/src/util/parseType.ts @@ -15,44 +15,48 @@ import { isInferredType, isIntrinsicType, isUnknownType, -} from './types'; +} from './types.js'; -export function parseType(t: JSONOutput.SomeType | JSONOutput.Type | string): string { - if (typeof t === 'string') { - return t; +export function parseType(someType: JSONOutput.SomeType | JSONOutput.Type | string): string { + if (typeof someType === 'string') { + return someType; } - if (isArrayType(t)) { - return `Array<${parseType(t.elementType)}>`; + if (isArrayType(someType)) { + return `Array<${parseType(someType.elementType)}>`; } - if (isConditionalType(t)) { - const { checkType, extendsType, trueType, falseType } = t; + if (isConditionalType(someType)) { + const { checkType, extendsType, trueType, falseType } = someType; return `${parseType(checkType)} extends ${parseType(extendsType)} ? ${parseType(trueType)} : ${parseType( falseType, )}`; } - if (isIndexedAccessType(t)) { - return `${parseType(t.objectType)}[${parseType(t.indexType)}]`; + if (isIndexedAccessType(someType)) { + return `${parseType(someType.objectType)}[${parseType(someType.indexType)}]`; } - if (isIntersectionType(t)) { - return t.types.map(parseType).join(' & '); + if (isIntersectionType(someType)) { + return someType.types.map(parseType).join(' & '); } - if (isPredicateType(t)) { - return (t.asserts ? 'asserts ' : '') + t.name + (t.targetType ? ` is ${parseType(t.targetType)}` : ''); + if (isPredicateType(someType)) { + return ( + (someType.asserts ? 'asserts ' : '') + + someType.name + + (someType.targetType ? ` is ${parseType(someType.targetType)}` : '') + ); } - if (isReferenceType(t)) { - return t.name + (t.typeArguments ? `<${t.typeArguments.map(parseType).join(', ')}>` : ''); + if (isReferenceType(someType)) { + return someType.name + (someType.typeArguments ? `<${someType.typeArguments.map(parseType).join(', ')}>` : ''); } - if (isReflectionType(t)) { + if (isReflectionType(someType)) { const obj: Record = {}; - const { children, signatures } = t.declaration!; + const { children, signatures } = someType.declaration!; // This is run when we're parsing interface-like declaration if (children && children.length > 0) { @@ -62,6 +66,7 @@ export function parseType(t: JSONOutput.SomeType | JSONOutput.Type | string): st obj[child.name] = parseType(type); } } + return `{\n${Object.entries(obj) .map(([key, value]) => `${key}: ${value as string}`) .join(',\n')}\n}`; @@ -69,43 +74,48 @@ export function parseType(t: JSONOutput.SomeType | JSONOutput.Type | string): st // This is run if we're parsing a function type if (signatures && signatures.length > 0) { - const s = signatures[0]; - const params = s?.parameters?.map((p) => `${p.name}: ${p.type ? parseType(p.type) : 'unknown'}`); - return `(${params?.join(', ') ?? '...args: unknown[]'}) => ${s?.type ? parseType(s.type) : 'unknown'}`; + const signature = signatures[0]; + const params = signature?.parameters?.map( + (param) => `${param.name}: ${param.type ? parseType(param.type) : 'unknown'}`, + ); + return `(${params?.join(', ') ?? '...args: unknown[]'}) => ${ + signature?.type ? parseType(signature.type) : 'unknown' + }`; } return '{}'; } - if (isLiteralType(t)) { - if (typeof t.value == 'string') { - return `'${t.value}'`; + if (isLiteralType(someType)) { + if (typeof someType.value === 'string') { + return `'${someType.value}'`; } + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `${t.value}`; + return `${someType.value}`; } - if (isTupleType(t)) { - return `[${(t.elements ?? []).map(parseType).join(', ')}]`; + if (isTupleType(someType)) { + return `[${(someType.elements ?? []).map(parseType).join(', ')}]`; } - if (isTypeOperatorType(t)) { - return `${t.operator} ${parseType(t.target)}`; + if (isTypeOperatorType(someType)) { + return `${someType.operator} ${parseType(someType.target)}`; } - if (isUnionType(t)) { - return t.types + if (isUnionType(someType)) { + return someType.types .map(parseType) - .filter((s) => Boolean(s) && s.trim().length > 0) + .filter((currentType) => Boolean(currentType) && currentType.trim().length > 0) .join(' | '); } - if (isQueryType(t)) { - return `(typeof ${parseType(t.queryType)})`; + if (isQueryType(someType)) { + return `(typeof ${parseType(someType.queryType)})`; } - if (isInferredType(t) || isIntrinsicType(t) || isUnknownType(t)) { - return t.name; + if (isInferredType(someType) || isIntrinsicType(someType) || isUnknownType(someType)) { + return someType.name; } return 'unknown'; diff --git a/packages/docgen/src/util/splitVarName.ts b/packages/docgen/src/util/splitVarName.ts index 7ab52bdc38a5..2f7cc82a7b67 100644 --- a/packages/docgen/src/util/splitVarName.ts +++ b/packages/docgen/src/util/splitVarName.ts @@ -1,10 +1,10 @@ +const isASymbol = (char: string) => '-!$%^&*()_+|~=`{}[]:;<>?,. '.includes(char); + export function splitVarName(str: string) { const res: string[][] = []; let currGroup: string[] = []; let currStr = ''; - const isASymbol = (char: string) => '-!$%^&*()_+|~=`{}[]:;<>?,. '.includes(char); - for (const char of str) { const currentlyInASymbolSection = isASymbol(currStr[0]!); const charIsASymbol = isASymbol(char); @@ -13,6 +13,7 @@ export function splitVarName(str: string) { if (char === '.') { continue; } + currGroup.push(currStr); currStr = char; @@ -24,6 +25,7 @@ export function splitVarName(str: string) { currStr += char; } } + currGroup.push(currStr); res.push(currGroup); diff --git a/packages/docgen/src/util/types.ts b/packages/docgen/src/util/types.ts index e9a3fc83611d..b2270fcb4031 100644 --- a/packages/docgen/src/util/types.ts +++ b/packages/docgen/src/util/types.ts @@ -2,66 +2,66 @@ import type { JSONOutput } from 'typedoc'; interface QueryType { - type: 'query'; queryType: JSONOutput.SomeType; + type: 'query'; } export function isArrayType(value: any): value is JSONOutput.ArrayType { - return typeof value == 'object' && value.type === 'array'; + return typeof value === 'object' && value.type === 'array'; } export function isConditionalType(value: any): value is JSONOutput.ConditionalType { - return typeof value == 'object' && value.type === 'conditional'; + return typeof value === 'object' && value.type === 'conditional'; } export function isIndexedAccessType(value: any): value is JSONOutput.IndexedAccessType { - return typeof value == 'object' && value.type === 'indexedAccess'; + return typeof value === 'object' && value.type === 'indexedAccess'; } export function isInferredType(value: any): value is JSONOutput.InferredType { - return typeof value == 'object' && value.type === 'inferred'; + return typeof value === 'object' && value.type === 'inferred'; } export function isIntersectionType(value: any): value is JSONOutput.IntersectionType { - return typeof value == 'object' && value.type === 'intersection'; + return typeof value === 'object' && value.type === 'intersection'; } export function isIntrinsicType(value: any): value is JSONOutput.IntrinsicType { - return typeof value == 'object' && value.type === 'intrinsic'; + return typeof value === 'object' && value.type === 'intrinsic'; } export function isPredicateType(value: any): value is JSONOutput.PredicateType { - return typeof value == 'object' && value.type === 'predicate'; + return typeof value === 'object' && value.type === 'predicate'; } export function isReferenceType(value: any): value is JSONOutput.ReferenceType { - return typeof value == 'object' && value.type === 'reference'; + return typeof value === 'object' && value.type === 'reference'; } export function isReflectionType(value: any): value is JSONOutput.ReflectionType { - return typeof value == 'object' && value.type === 'reflection'; + return typeof value === 'object' && value.type === 'reflection'; } export function isLiteralType(value: any): value is JSONOutput.LiteralType { - return typeof value == 'object' && value.type === 'literal'; + return typeof value === 'object' && value.type === 'literal'; } export function isTupleType(value: any): value is JSONOutput.TupleType { - return typeof value == 'object' && value.type === 'tuple'; + return typeof value === 'object' && value.type === 'tuple'; } export function isTypeOperatorType(value: any): value is JSONOutput.TypeOperatorType { - return typeof value == 'object' && value.type === 'typeOperator'; + return typeof value === 'object' && value.type === 'typeOperator'; } export function isUnionType(value: any): value is JSONOutput.UnionType { - return typeof value == 'object' && value.type === 'union'; + return typeof value === 'object' && value.type === 'union'; } export function isUnknownType(value: any): value is JSONOutput.UnknownType { - return typeof value == 'object' && value.type === 'unknown'; + return typeof value === 'object' && value.type === 'unknown'; } export function isQueryType(value: any): value is QueryType { - return typeof value == 'object' && value.type === 'query'; + return typeof value === 'object' && value.type === 'query'; } diff --git a/packages/proxy-container/package.json b/packages/proxy-container/package.json index 7de606b19fc7..d92c99a2d673 100644 --- a/packages/proxy-container/package.json +++ b/packages/proxy-container/package.json @@ -50,13 +50,8 @@ }, "devDependencies": { "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/proxy-container/src/index.ts b/packages/proxy-container/src/index.ts index 8ddca58417c4..48284a8c1c44 100644 --- a/packages/proxy-container/src/index.ts +++ b/packages/proxy-container/src/index.ts @@ -1,4 +1,5 @@ import { createServer } from 'node:http'; +import process from 'node:process'; import { proxyRequests } from '@discordjs/proxy'; import { REST } from '@discordjs/rest'; @@ -11,5 +12,5 @@ const api = new REST({ rejectOnRateLimit: () => true, retries: 0 }).setToken(pro // eslint-disable-next-line @typescript-eslint/no-misused-promises const server = createServer(proxyRequests(api)); -const port = parseInt(process.env.PORT ?? '8080', 10); +const port = Number.parseInt(process.env.PORT ?? '8080', 10); server.listen(port, () => console.log(`Listening on port ${port}`)); diff --git a/packages/proxy/__tests__/proxyRequests.test.ts b/packages/proxy/__tests__/proxyRequests.test.ts index e010fec7bcc0..f956958be8b5 100644 --- a/packages/proxy/__tests__/proxyRequests.test.ts +++ b/packages/proxy/__tests__/proxyRequests.test.ts @@ -1,10 +1,10 @@ import { createServer } from 'node:http'; import { REST } from '@discordjs/rest'; import supertest from 'supertest'; -import { MockAgent, Interceptable, setGlobalDispatcher } from 'undici'; +import { MockAgent, setGlobalDispatcher, type Interceptable } from 'undici'; import type { MockInterceptor } from 'undici/types/mock-interceptor'; import { beforeEach, afterAll, afterEach, test, expect } from 'vitest'; -import { proxyRequests } from '../src'; +import { proxyRequests } from '../src/index.js'; let mockAgent: MockAgent; let mockPool: Interceptable; diff --git a/packages/proxy/package.json b/packages/proxy/package.json index 5aa3fdff88d3..8d2d3649540b 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -64,15 +64,10 @@ "@microsoft/api-extractor": "^7.29.5", "@types/node": "^16.11.56", "@types/supertest": "^2.0.12", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@vitest/coverage-c8": "^0.22.1", "downlevel-dts": "^0.10.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "supertest": "^6.2.4", diff --git a/packages/proxy/src/handlers/proxyRequests.ts b/packages/proxy/src/handlers/proxyRequests.ts index 823c9c14c615..1efb8601f355 100644 --- a/packages/proxy/src/handlers/proxyRequests.ts +++ b/packages/proxy/src/handlers/proxyRequests.ts @@ -1,11 +1,18 @@ import { URL } from 'node:url'; -import { DiscordAPIError, HTTPError, RateLimitError, RequestMethod, REST, RouteLike } from '@discordjs/rest'; +import { + DiscordAPIError, + HTTPError, + RateLimitError, + type RequestMethod, + type REST, + type RouteLike, +} from '@discordjs/rest'; import { populateAbortErrorResponse, populateGeneralErrorResponse, populateSuccessfulResponse, populateRatelimitErrorResponse, -} from '../util/responseHelpers'; +} from '../util/responseHelpers.js'; import type { RequestHandler } from '../util/util'; /** @@ -25,6 +32,7 @@ export function proxyRequests(rest: REST): RequestHandler { // The 2nd parameter is here so the URL constructor doesn't complain about an "invalid url" when the origin is missing // we don't actually care about the origin and the value passed is irrelevant + // eslint-disable-next-line prefer-named-capture-group, unicorn/no-unsafe-regex const fullRoute = new URL(url, 'http://noop').pathname.replace(/^\/api(\/v\d+)?/, '') as RouteLike; try { diff --git a/packages/proxy/src/index.ts b/packages/proxy/src/index.ts index 9faf38000709..3c5391723f48 100644 --- a/packages/proxy/src/index.ts +++ b/packages/proxy/src/index.ts @@ -1,3 +1,3 @@ -export * from './handlers/proxyRequests'; -export * from './util/responseHelpers'; -export type { RequestHandler } from './util/util'; +export * from './handlers/proxyRequests.js'; +export * from './util/responseHelpers.js'; +export type { RequestHandler } from './util/util.js'; diff --git a/packages/proxy/src/util/responseHelpers.ts b/packages/proxy/src/util/responseHelpers.ts index 638ed92a98fe..7127ab633426 100644 --- a/packages/proxy/src/util/responseHelpers.ts +++ b/packages/proxy/src/util/responseHelpers.ts @@ -47,7 +47,7 @@ export function populateGeneralErrorResponse(res: ServerResponse, error: Discord */ export function populateRatelimitErrorResponse(res: ServerResponse, error: RateLimitError): void { res.statusCode = 429; - res.setHeader('Retry-After', error.timeToReset / 1000); + res.setHeader('Retry-After', error.timeToReset / 1_000); } /** diff --git a/packages/proxy/src/util/util.ts b/packages/proxy/src/util/util.ts index 79e06a9ee213..58f77681df17 100644 --- a/packages/proxy/src/util/util.ts +++ b/packages/proxy/src/util/util.ts @@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; /** * Represents a potentially awaitable value */ -export type Awaitable = T | PromiseLike; +export type Awaitable = PromiseLike | T; /** * Represents a simple HTTP request handler diff --git a/packages/rest/__tests__/CDN.test.ts b/packages/rest/__tests__/CDN.test.ts index c7c6d413f6db..8efe234e587d 100644 --- a/packages/rest/__tests__/CDN.test.ts +++ b/packages/rest/__tests__/CDN.test.ts @@ -1,11 +1,11 @@ import { test, expect } from 'vitest'; -import { CDN } from '../src'; +import { CDN } from '../src/index.js'; const base = 'https://discord.com'; const id = '123456'; const hash = 'abcdef'; const animatedHash = 'a_bcdef'; -const defaultAvatar = 1234 % 5; +const defaultAvatar = 1_234 % 5; const cdn = new CDN(base); diff --git a/packages/rest/__tests__/DiscordAPIError.test.ts b/packages/rest/__tests__/DiscordAPIError.test.ts index 604df7216f77..05fe6703a2c6 100644 --- a/packages/rest/__tests__/DiscordAPIError.test.ts +++ b/packages/rest/__tests__/DiscordAPIError.test.ts @@ -1,5 +1,6 @@ +import { URLSearchParams } from 'node:url'; import { test, expect } from 'vitest'; -import { DiscordAPIError } from '../src'; +import { DiscordAPIError } from '../src/index.js'; test('Unauthorized', () => { const error = new DiscordAPIError( @@ -27,13 +28,13 @@ test('Unauthorized', () => { test('Invalid Form Body Error (error.{property}._errors.{index})', () => { const error = new DiscordAPIError( { - code: 50035, + code: 50_035, errors: { username: { _errors: [{ code: 'BASE_TYPE_BAD_LENGTH', message: 'Must be between 2 and 32 in length.' }] }, }, message: 'Invalid Form Body', }, - 50035, + 50_035, 400, 'PATCH', 'https://discord.com/api/v10/users/@me', @@ -45,7 +46,7 @@ test('Invalid Form Body Error (error.{property}._errors.{index})', () => { }, ); - expect(error.code).toEqual(50035); + expect(error.code).toEqual(50_035); expect(error.message).toEqual( ['Invalid Form Body', 'username[BASE_TYPE_BAD_LENGTH]: Must be between 2 and 32 in length.'].join('\n'), ); @@ -60,7 +61,7 @@ test('Invalid Form Body Error (error.{property}._errors.{index})', () => { test('Invalid FormFields Error (error.errors.{property}.{property}.{index}.{property}._errors.{index})', () => { const error = new DiscordAPIError( { - code: 50035, + code: 50_035, errors: { embed: { fields: { '0': { value: { _errors: [{ code: 'BASE_TYPE_REQUIRED', message: 'This field is required' }] } } }, @@ -68,14 +69,14 @@ test('Invalid FormFields Error (error.errors.{property}.{property}.{index}.{prop }, message: 'Invalid Form Body', }, - 50035, + 50_035, 400, 'POST', 'https://discord.com/api/v10/channels/:id', {}, ); - expect(error.code).toEqual(50035); + expect(error.code).toEqual(50_035); expect(error.message).toEqual( ['Invalid Form Body', 'embed.fields[0].value[BASE_TYPE_REQUIRED]: This field is required'].join('\n'), ); @@ -88,7 +89,7 @@ test('Invalid FormFields Error (error.errors.{property}.{property}.{index}.{prop test('Invalid FormFields Error (error.errors.{property}.{property}._errors.{index}._errors)', () => { const error = new DiscordAPIError( { - code: 50035, + code: 50_035, errors: { form_fields: { label: { _errors: [{ _errors: [{ code: 'BASE_TYPE_REQUIRED', message: 'This field is required' }] }] }, @@ -96,14 +97,14 @@ test('Invalid FormFields Error (error.errors.{property}.{property}._errors.{inde }, message: 'Invalid Form Body', }, - 50035, + 50_035, 400, 'PATCH', 'https://discord.com/api/v10/guilds/:id', {}, ); - expect(error.code).toEqual(50035); + expect(error.code).toEqual(50_035); expect(error.message).toEqual( ['Invalid Form Body', 'form_fields.label[0][BASE_TYPE_REQUIRED]: This field is required'].join('\n'), ); diff --git a/packages/rest/__tests__/REST.test.ts b/packages/rest/__tests__/REST.test.ts index 946771656cd3..ee9c854a137b 100644 --- a/packages/rest/__tests__/REST.test.ts +++ b/packages/rest/__tests__/REST.test.ts @@ -1,10 +1,14 @@ +import { Buffer } from 'node:buffer'; +import { URLSearchParams } from 'node:url'; import { DiscordSnowflake } from '@sapphire/snowflake'; -import { Routes, Snowflake } from 'discord-api-types/v10'; -import { File, FormData, MockAgent, setGlobalDispatcher } from 'undici'; +import type { Snowflake } from 'discord-api-types/v10'; +import { Routes } from 'discord-api-types/v10'; +import type { FormData } from 'undici'; +import { File, MockAgent, setGlobalDispatcher } from 'undici'; import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor'; import { beforeEach, afterEach, test, expect } from 'vitest'; -import { genPath } from './util'; -import { REST } from '../src'; +import { REST } from '../src/index.js'; +import { genPath } from './util.js'; const newSnowflake: Snowflake = DiscordSnowflake.generate().toString(); @@ -142,7 +146,7 @@ test('getQuery', async () => { expect( await api.get('/getQuery', { - query: query, + query, }), ).toStrictEqual({ test: true }); }); @@ -153,8 +157,8 @@ test('getAuth', async () => { path: genPath('/getAuth'), method: 'GET', }) - .reply((t) => ({ - data: { auth: (t.headers as unknown as Record)['Authorization'] ?? null }, + .reply((from) => ({ + data: { auth: (from.headers as unknown as Record).Authorization ?? null }, statusCode: 200, responseOptions, })) @@ -184,8 +188,8 @@ test('getReason', async () => { path: genPath('/getReason'), method: 'GET', }) - .reply((t) => ({ - data: { reason: (t.headers as unknown as Record)['X-Audit-Log-Reason'] ?? null }, + .reply((from) => ({ + data: { reason: (from.headers as unknown as Record)['X-Audit-Log-Reason'] ?? null }, statusCode: 200, responseOptions, })) @@ -215,8 +219,8 @@ test('urlEncoded', async () => { path: genPath('/urlEncoded'), method: 'POST', }) - .reply((t) => ({ - data: t.body!, + .reply((from) => ({ + data: from.body!, statusCode: 200, })); @@ -245,8 +249,8 @@ test('postEcho', async () => { path: genPath('/postEcho'), method: 'POST', }) - .reply((t) => ({ - data: t.body!, + .reply((from) => ({ + data: from.body!, statusCode: 200, responseOptions, })); @@ -260,8 +264,8 @@ test('201 status code', async () => { path: genPath('/postNon200StatusCode'), method: 'POST', }) - .reply((t) => ({ - data: t.body!, + .reply((from) => ({ + data: from.body!, statusCode: 201, responseOptions, })); diff --git a/packages/rest/__tests__/RequestHandler.test.ts b/packages/rest/__tests__/RequestHandler.test.ts index 3dc5bbc06702..2bd7330a587a 100644 --- a/packages/rest/__tests__/RequestHandler.test.ts +++ b/packages/rest/__tests__/RequestHandler.test.ts @@ -1,15 +1,18 @@ +/* eslint-disable id-length */ +/* eslint-disable promise/prefer-await-to-then */ import { performance } from 'node:perf_hooks'; +import { setInterval, clearInterval } from 'node:timers'; import { MockAgent, setGlobalDispatcher } from 'undici'; import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor'; import { beforeEach, afterEach, test, expect, vitest } from 'vitest'; -import { genPath } from './util'; -import { DiscordAPIError, HTTPError, RateLimitError, REST, RESTEvents } from '../src'; +import { DiscordAPIError, HTTPError, RateLimitError, REST, RESTEvents } from '../src/index.js'; +import { genPath } from './util.js'; let mockAgent: MockAgent; let mockPool: Interceptable; -const api = new REST({ timeout: 2000, offset: 5 }).setToken('A-Very-Fake-Token'); -const invalidAuthApi = new REST({ timeout: 2000 }).setToken('Definitely-Not-A-Fake-Token'); +const api = new REST({ timeout: 2_000, offset: 5 }).setToken('A-Very-Fake-Token'); +const invalidAuthApi = new REST({ timeout: 2_000 }).setToken('Definitely-Not-A-Fake-Token'); const rateLimitErrorApi = new REST({ rejectOnRateLimit: ['/channels'] }).setToken('Obviously-Not-A-Fake-Token'); beforeEach(() => { @@ -52,7 +55,7 @@ const sublimitIntervals: { }; const sublimit = { body: { name: 'newname' } }; -const noSublimit = { body: { bitrate: 40000 } }; +const noSublimit = { body: { bitrate: 40_000 } }; function startSublimitIntervals() { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -63,13 +66,14 @@ function startSublimitIntervals() { sublimitResetAfter = Date.now() + 250; }, 250); } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!sublimitIntervals.retry) { - retryAfter = Date.now() + 1000; + retryAfter = Date.now() + 1_000; sublimitIntervals.retry = setInterval(() => { sublimitHits = 0; - retryAfter = Date.now() + 1000; - }, 1000); + retryAfter = Date.now() + 1_000; + }, 1_000); } } @@ -80,7 +84,7 @@ test('Significant Invalid Requests', async () => { path: genPath('/badRequest'), method: 'GET', }) - .reply(403, { message: 'Missing Permissions', code: 50013 }, responseOptions) + .reply(403, { message: 'Missing Permissions', code: 50_013 }, responseOptions) .times(10); const invalidListener = vitest.fn(); @@ -89,6 +93,7 @@ test('Significant Invalid Requests', async () => { // Ensure listeners on REST do not get double added api.on(RESTEvents.InvalidRequestWarning, invalidListener2); api.off(RESTEvents.InvalidRequestWarning, invalidListener2); + const [a, b, c, d, e] = [ api.get('/badRequest'), api.get('/badRequest'), @@ -102,7 +107,9 @@ test('Significant Invalid Requests', async () => { await expect(d).rejects.toThrowError('Missing Permissions'); await expect(e).rejects.toThrowError('Missing Permissions'); expect(invalidListener).toHaveBeenCalledTimes(0); + // eslint-disable-next-line require-atomic-updates api.requestManager.options.invalidRequestWarningInterval = 2; + const [f, g, h, i, j] = [ api.get('/badRequest'), api.get('/badRequest'), @@ -137,7 +144,7 @@ test('Handle standard rate limits', async () => { headers: { 'x-ratelimit-limit': '1', 'x-ratelimit-remaining': '0', - 'x-ratelimit-reset-after': ((resetAfter - Date.now()) / 1000).toString(), + 'x-ratelimit-reset-after': ((resetAfter - Date.now()) / 1_000).toString(), 'x-ratelimit-bucket': '80c17d2f203122d936070c88c8d10f33', via: '1.1 google', }, @@ -150,7 +157,7 @@ test('Handle standard rate limits', async () => { data: { limit: '1', remaining: '0', - resetAfter: (resetAfter / 1000).toString(), + resetAfter: (resetAfter / 1_000).toString(), bucket: '80c17d2f203122d936070c88c8d10f33', retryAfter: (resetAfter - Date.now()).toString(), }, @@ -158,7 +165,7 @@ test('Handle standard rate limits', async () => { headers: { 'x-ratelimit-limit': '1', 'x-ratelimit-remaining': '0', - 'x-ratelimit-reset-after': ((resetAfter - Date.now()) / 1000).toString(), + 'x-ratelimit-reset-after': ((resetAfter - Date.now()) / 1_000).toString(), 'x-ratelimit-bucket': '80c17d2f203122d936070c88c8d10f33', 'retry-after': (resetAfter - Date.now()).toString(), via: '1.1 google', @@ -187,8 +194,8 @@ test('Handle sublimits', async () => { path: genPath('/channels/:id'), method: 'PATCH', }) - .reply((t) => { - const body = JSON.parse(t.body as string) as Record; + .reply((from) => { + const body = JSON.parse(from.body as string) as Record; if ('name' in body || 'topic' in body) { sublimitHits += 1; @@ -204,7 +211,7 @@ test('Handle sublimits', async () => { headers: { 'x-ratelimit-limit': '10', 'x-ratelimit-remaining': `${10 - sublimitRequests}`, - 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(), + 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1_000).toString(), via: '1.1 google', }, }, @@ -216,15 +223,15 @@ test('Handle sublimits', async () => { data: { limit: '10', remaining: `${10 - sublimitRequests}`, - resetAfter: (sublimitResetAfter / 1000).toString(), - retryAfter: ((retryAfter - Date.now()) / 1000).toString(), + resetAfter: (sublimitResetAfter / 1_000).toString(), + retryAfter: ((retryAfter - Date.now()) / 1_000).toString(), }, responseOptions: { headers: { 'x-ratelimit-limit': '10', 'x-ratelimit-remaining': `${10 - sublimitRequests}`, - 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(), - 'retry-after': ((retryAfter - Date.now()) / 1000).toString(), + 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1_000).toString(), + 'retry-after': ((retryAfter - Date.now()) / 1_000).toString(), via: '1.1 google', ...responseOptions.headers, }, @@ -243,7 +250,7 @@ test('Handle sublimits', async () => { headers: { 'x-ratelimit-limit': '10', 'x-ratelimit-remaining': `${10 - sublimitRequests}`, - 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(), + 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1_000).toString(), via: '1.1 google', }, }, @@ -255,15 +262,15 @@ test('Handle sublimits', async () => { data: { limit: '10', remaining: `${10 - sublimitRequests}`, - resetAfter: (sublimitResetAfter / 1000).toString(), - retryAfter: ((sublimitResetAfter - Date.now()) / 1000).toString(), + resetAfter: (sublimitResetAfter / 1_000).toString(), + retryAfter: ((sublimitResetAfter - Date.now()) / 1_000).toString(), }, responseOptions: { headers: { 'x-ratelimit-limit': '10', 'x-ratelimit-remaining': `${10 - sublimitRequests}`, - 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(), - 'retry-after': ((sublimitResetAfter - Date.now()) / 1000).toString(), + 'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1_000).toString(), + 'retry-after': ((sublimitResetAfter - Date.now()) / 1_000).toString(), via: '1.1 google', ...responseOptions.headers, }, @@ -294,6 +301,7 @@ test('Handle sublimits', async () => { api.patch('/channels/:id', sublimit).then(() => Date.now()), api.patch('/channels/:id', noSublimit).then(() => Date.now()), ]); // For additional sublimited checks + const e = await eP; expect(a).toBeLessThanOrEqual(b); @@ -314,6 +322,7 @@ test('Handle sublimits', async () => { rateLimitErrorApi.patch('/channels/:id', sublimit), rateLimitErrorApi.patch('/channels/:id', sublimit), ]; + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(aP2).resolves; await expect(bP2).rejects.toThrowError(); await expect(bP2).rejects.toBeInstanceOf(RateLimitError); @@ -364,8 +373,8 @@ test('Handle unexpected 429', async () => { expect(await unexepectedSublimit).toStrictEqual({ test: true }); expect(await queuedSublimit).toStrictEqual({ test: true }); - expect(performance.now()).toBeGreaterThanOrEqual(previous + 1000); - // @ts-expect-error + expect(performance.now()).toBeGreaterThanOrEqual(previous + 1_000); + // @ts-expect-error: This is intentional expect(secondResolvedTime).toBeGreaterThan(firstResolvedTime); }); @@ -400,7 +409,7 @@ test('Handle unexpected 429 cloudflare', async () => { const previous = Date.now(); expect(await api.get('/unexpected-cf')).toStrictEqual({ test: true }); - expect(Date.now()).toBeGreaterThanOrEqual(previous + 1000); + expect(Date.now()).toBeGreaterThanOrEqual(previous + 1_000); }); test('Handle global rate limits', async () => { @@ -486,7 +495,7 @@ test('server responding too slow', async () => { const promise = api2.get('/slow'); await expect(promise).rejects.toThrowError('Request aborted'); -}, 1000); +}, 1_000); test('Unauthorized', async () => { mockPool @@ -518,7 +527,7 @@ test('Bad Request', async () => { path: genPath('/badRequest'), method: 'GET', }) - .reply(403, { message: 'Missing Permissions', code: 50013 }, responseOptions); + .reply(403, { message: 'Missing Permissions', code: 50_013 }, responseOptions); const promise = api.get('/badRequest'); await expect(promise).rejects.toThrowError('Missing Permissions'); diff --git a/packages/rest/__tests__/RequestManager.test.ts b/packages/rest/__tests__/RequestManager.test.ts index 947a861a177b..73dcf6285365 100644 --- a/packages/rest/__tests__/RequestManager.test.ts +++ b/packages/rest/__tests__/RequestManager.test.ts @@ -1,8 +1,7 @@ -import { MockAgent, setGlobalDispatcher } from 'undici'; -import type { Interceptable } from 'undici/types/mock-interceptor'; +import { MockAgent, setGlobalDispatcher, type Interceptable } from 'undici'; import { beforeEach, afterEach, test, expect } from 'vitest'; -import { genPath } from './util'; -import { REST } from '../src'; +import { REST } from '../src/index.js'; +import { genPath } from './util.js'; const api = new REST(); @@ -35,7 +34,7 @@ test('no token', async () => { }); test('negative offset', () => { - const badREST = new REST({ offset: -5000 }); + const badREST = new REST({ offset: -5_000 }); expect(badREST.requestManager.options.offset).toEqual(0); }); diff --git a/packages/rest/__tests__/Util.test.ts b/packages/rest/__tests__/Util.test.ts index d323efc7d7f1..76504b974962 100644 --- a/packages/rest/__tests__/Util.test.ts +++ b/packages/rest/__tests__/Util.test.ts @@ -1,6 +1,7 @@ -import { Blob } from 'node:buffer'; +import { Blob, Buffer } from 'node:buffer'; +import { URLSearchParams } from 'node:url'; import { test, expect } from 'vitest'; -import { resolveBody, parseHeader } from '../src/lib/utils/utils'; +import { resolveBody, parseHeader } from '../src/lib/utils/utils.js'; test('GIVEN string parseHeader returns string', () => { const header = 'application/json'; @@ -37,7 +38,7 @@ test('resolveBody', async () => { const iterable: Iterable = { *[Symbol.iterator]() { - for (let i = 0; i < 3; i++) { + for (let index = 0; index < 3; index++) { yield new Uint8Array([1, 2, 3]); } }, @@ -46,15 +47,15 @@ test('resolveBody', async () => { const asyncIterable: AsyncIterable = { [Symbol.asyncIterator]() { - let i = 0; + let index = 0; return { - next() { - if (i < 3) { - i++; - return Promise.resolve({ value: new Uint8Array([1, 2, 3]), done: false }); + async next() { + if (index < 3) { + index++; + return { value: new Uint8Array([1, 2, 3]), done: false }; } - return Promise.resolve({ value: undefined, done: true }); + return { value: undefined, done: true }; }, }; }, diff --git a/packages/rest/__tests__/util.ts b/packages/rest/__tests__/util.ts index 6ef1c435b316..a1241cc69cd0 100644 --- a/packages/rest/__tests__/util.ts +++ b/packages/rest/__tests__/util.ts @@ -1,4 +1,4 @@ -import { DefaultRestOptions } from '../src'; +import { DefaultRestOptions } from '../src/index.js'; export function genPath(path: `/${string}`) { return `/api/v${DefaultRestOptions.version}${path}` as const; diff --git a/packages/rest/__tests__/utils.test.ts b/packages/rest/__tests__/utils.test.ts index b5ded819f109..90482b9ada56 100644 --- a/packages/rest/__tests__/utils.test.ts +++ b/packages/rest/__tests__/utils.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { makeURLSearchParams } from '../src'; +import { makeURLSearchParams } from '../src/index.js'; describe('makeURLSearchParams', () => { test('GIVEN undefined THEN returns empty URLSearchParams', () => { @@ -41,7 +41,7 @@ describe('makeURLSearchParams', () => { describe('objects', () => { test('GIVEN a record of date values THEN URLSearchParams with ISO string values', () => { - const params = makeURLSearchParams({ before: new Date('2022-04-04T15:43:05.108Z'), after: new Date(NaN) }); + const params = makeURLSearchParams({ before: new Date('2022-04-04T15:43:05.108Z'), after: new Date(Number.NaN) }); expect([...params.entries()]).toEqual([['before', '2022-04-04T15:43:05.108Z']]); }); diff --git a/packages/rest/package.json b/packages/rest/package.json index 6d9351d1fc16..6d117b113b98 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -65,16 +65,10 @@ "@favware/cliff-jumper": "^1.8.7", "@microsoft/api-extractor": "^7.29.5", "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@vitest/coverage-c8": "^0.22.1", "downlevel-dts": "^0.10.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-tsdoc": "^0.2.16", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index eee7ee0c1791..ab6fa949ba67 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -1,8 +1,8 @@ -export * from './lib/CDN'; -export * from './lib/errors/DiscordAPIError'; -export * from './lib/errors/HTTPError'; -export * from './lib/errors/RateLimitError'; -export * from './lib/RequestManager'; -export * from './lib/REST'; -export * from './lib/utils/constants'; -export { makeURLSearchParams, parseResponse } from './lib/utils/utils'; +export * from './lib/CDN.js'; +export * from './lib/errors/DiscordAPIError.js'; +export * from './lib/errors/HTTPError.js'; +export * from './lib/errors/RateLimitError.js'; +export * from './lib/RequestManager.js'; +export * from './lib/REST.js'; +export * from './lib/utils/constants.js'; +export { makeURLSearchParams, parseResponse } from './lib/utils/utils.js'; diff --git a/packages/rest/src/lib/CDN.ts b/packages/rest/src/lib/CDN.ts index 0385adc03d4d..327225fd4aed 100644 --- a/packages/rest/src/lib/CDN.ts +++ b/packages/rest/src/lib/CDN.ts @@ -1,12 +1,14 @@ +/* eslint-disable jsdoc/check-param-names */ +import { URL } from 'node:url'; import { ALLOWED_EXTENSIONS, ALLOWED_SIZES, ALLOWED_STICKER_EXTENSIONS, DefaultRestOptions, - ImageExtension, - ImageSize, - StickerExtension, -} from './utils/constants'; + type ImageExtension, + type ImageSize, + type StickerExtension, +} from './utils/constants.js'; /** * The options used for image URLs @@ -38,6 +40,10 @@ export interface ImageURLOptions extends BaseImageURLOptions { * The options to use when making a CDN URL */ export interface MakeURLOptions { + /** + * The allowed extensions that can be used + */ + allowedExtensions?: readonly string[]; /** * The extension to use for the image URL * @@ -48,10 +54,6 @@ export interface MakeURLOptions { * The size specified in the image URL */ size?: ImageSize; - /** - * The allowed extensions that can be used - */ - allowedExtensions?: ReadonlyArray; } /** @@ -192,6 +194,7 @@ export class CDN { /** * Generates a URL for the icon of a role + * * @param roleId - The id of the role that has the icon * @param roleIconHash - The hash provided by Discord for this role icon * @param options - Optional options for the role icon @@ -285,6 +288,7 @@ export class CDN { route: string, { allowedExtensions = ALLOWED_EXTENSIONS, extension = 'webp', size }: Readonly = {}, ): string { + // eslint-disable-next-line no-param-reassign extension = String(extension).toLowerCase(); if (!allowedExtensions.includes(extension)) { diff --git a/packages/rest/src/lib/REST.ts b/packages/rest/src/lib/REST.ts index adb148a487a3..7b2a1afd4060 100644 --- a/packages/rest/src/lib/REST.ts +++ b/packages/rest/src/lib/REST.ts @@ -1,19 +1,19 @@ import { EventEmitter } from 'node:events'; import type { Collection } from '@discordjs/collection'; import type { request, Dispatcher } from 'undici'; -import { CDN } from './CDN'; +import { CDN } from './CDN.js'; import { - HandlerRequestData, - InternalRequest, - RequestData, RequestManager, RequestMethod, - RouteLike, -} from './RequestManager'; -import type { HashData } from './RequestManager'; + type HashData, + type HandlerRequestData, + type InternalRequest, + type RequestData, + type RouteLike, +} from './RequestManager.js'; import type { IHandler } from './handlers/IHandler'; -import { DefaultRestOptions, RESTEvents } from './utils/constants'; -import { parseResponse } from './utils/utils'; +import { DefaultRestOptions, RESTEvents } from './utils/constants.js'; +import { parseResponse } from './utils/utils.js'; /** * Options to be passed when creating the REST instance @@ -25,6 +25,7 @@ export interface RESTOptions { agent: Dispatcher; /** * The base api path, without version + * * @defaultValue `'https://discord.com/api'` */ api: string; @@ -34,13 +35,37 @@ export interface RESTOptions { * * @defaultValue `'Bot'` */ - authPrefix: 'Bot' | 'Bearer'; + authPrefix: 'Bearer' | 'Bot'; /** * The cdn path * * @defaultValue 'https://cdn.discordapp.com' */ cdn: string; + /** + * How many requests to allow sending per second (Infinity for unlimited, 50 for the standard global limit used by Discord) + * + * @defaultValue `50` + */ + globalRequestsPerSecond: number; + /** + * The amount of time in milliseconds that passes between each hash sweep. (defaults to 1h) + * + * @defaultValue `3_600_000` + */ + handlerSweepInterval: number; + /** + * The maximum amount of time a hash can exist in milliseconds without being hit with a request (defaults to 24h) + * + * @defaultValue `86_400_000` + */ + hashLifetime: number; + /** + * The amount of time in milliseconds that passes between each hash sweep. (defaults to 4h) + * + * @defaultValue `14_400_000` + */ + hashSweepInterval: number; /** * Additional headers to send for all API requests * @@ -54,12 +79,6 @@ export interface RESTOptions { * @defaultValue `0` */ invalidRequestWarningInterval: number; - /** - * How many requests to allow sending per second (Infinity for unlimited, 50 for the standard global limit used by Discord) - * - * @defaultValue `50` - */ - globalRequestsPerSecond: number; /** * The extra offset to add to rate limits in milliseconds * @@ -74,7 +93,7 @@ export interface RESTOptions { * * @defaultValue `null` */ - rejectOnRateLimit: string[] | RateLimitQueueFilter | null; + rejectOnRateLimit: RateLimitQueueFilter | string[] | null; /** * The number of retries for errors with the 500 code, or errors * that timeout @@ -100,24 +119,6 @@ export interface RESTOptions { * @defaultValue `'10'` */ version: string; - /** - * The amount of time in milliseconds that passes between each hash sweep. (defaults to 4h) - * - * @defaultValue `14_400_000` - */ - hashSweepInterval: number; - /** - * The maximum amount of time a hash can exist in milliseconds without being hit with a request (defaults to 24h) - * - * @defaultValue `86_400_000` - */ - hashLifetime: number; - /** - * The amount of time in milliseconds that passes between each hash sweep. (defaults to 1h) - * - * @defaultValue `3_600_000` - */ - handlerSweepInterval: number; } /** @@ -125,72 +126,72 @@ export interface RESTOptions { */ export interface RateLimitData { /** - * The time, in milliseconds, until the request-lock is reset + * Whether the rate limit that was reached was the global limit */ - timeToReset: number; + global: boolean; /** - * The amount of requests we can perform before locking requests + * The bucket hash for this request */ - limit: number; + hash: string; /** - * The HTTP method being performed + * The amount of requests we can perform before locking requests */ - method: string; + limit: number; /** - * The bucket hash for this request + * The major parameter of the route + * + * For example, in `/channels/x`, this will be `x`. + * If there is no major parameter (e.g: `/bot/gateway`) this will be `global`. */ - hash: string; + majorParameter: string; /** - * The full URL for this request + * The HTTP method being performed */ - url: string; + method: string; /** * The route being hit in this request */ route: string; /** - * The major parameter of the route - * - * For example, in `/channels/x`, this will be `x`. - * If there is no major parameter (e.g: `/bot/gateway`) this will be `global`. + * The time, in milliseconds, until the request-lock is reset */ - majorParameter: string; + timeToReset: number; /** - * Whether the rate limit that was reached was the global limit + * The full URL for this request */ - global: boolean; + url: string; } /** * A function that determines whether the rate limit hit should throw an Error */ -export type RateLimitQueueFilter = (rateLimitData: RateLimitData) => boolean | Promise; +export type RateLimitQueueFilter = (rateLimitData: RateLimitData) => Promise | boolean; export interface APIRequest { /** - * The HTTP method used in this request - */ - method: string; - /** - * The full path used to make the request + * The data that was used to form the body of this request */ - path: RouteLike; + data: HandlerRequestData; /** - * The API route identifying the ratelimit for this request + * The HTTP method used in this request */ - route: string; + method: string; /** * Additional HTTP options for this request */ options: RequestOptions; /** - * The data that was used to form the body of this request + * The full path used to make the request */ - data: HandlerRequestData; + path: RouteLike; /** * The number of times this request has been attempted */ retries: number; + /** + * The API route identifying the ratelimit for this request + */ + route: string; } export interface InvalidRequestWarningData { @@ -205,29 +206,29 @@ export interface InvalidRequestWarningData { } export interface RestEvents { + handlerSweep: [sweptHandlers: Collection]; + hashSweep: [sweptHashes: Collection]; invalidRequestWarning: [invalidRequestInfo: InvalidRequestWarningData]; - restDebug: [info: string]; - rateLimited: [rateLimitInfo: RateLimitData]; - response: [request: APIRequest, response: Dispatcher.ResponseData]; newListener: [name: string, listener: (...args: any) => void]; + rateLimited: [rateLimitInfo: RateLimitData]; removeListener: [name: string, listener: (...args: any) => void]; - hashSweep: [sweptHashes: Collection]; - handlerSweep: [sweptHandlers: Collection]; + response: [request: APIRequest, response: Dispatcher.ResponseData]; + restDebug: [info: string]; } export interface REST { - on: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & - ((event: Exclude, listener: (...args: any[]) => void) => this); - - once: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & - ((event: Exclude, listener: (...args: any[]) => void) => this); - emit: ((event: K, ...args: RestEvents[K]) => boolean) & ((event: Exclude, ...args: any[]) => boolean); off: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & ((event: Exclude, listener: (...args: any[]) => void) => this); + on: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & + ((event: Exclude, listener: (...args: any[]) => void) => this); + + once: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & + ((event: Exclude, listener: (...args: any[]) => void) => this); + removeAllListeners: ((event?: K) => this) & ((event?: Exclude) => this); } @@ -236,6 +237,7 @@ export type RequestOptions = Exclude[1], undefined>; export class REST extends EventEmitter { public readonly cdn: CDN; + public readonly requestManager: RequestManager; public constructor(options: Partial = {}) { @@ -288,7 +290,7 @@ export class REST extends EventEmitter { * @param fullRoute - The full route to query * @param options - Optional request options */ - public get(fullRoute: RouteLike, options: RequestData = {}) { + public async get(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Get }); } @@ -298,7 +300,7 @@ export class REST extends EventEmitter { * @param fullRoute - The full route to query * @param options - Optional request options */ - public delete(fullRoute: RouteLike, options: RequestData = {}) { + public async delete(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Delete }); } @@ -308,7 +310,7 @@ export class REST extends EventEmitter { * @param fullRoute - The full route to query * @param options - Optional request options */ - public post(fullRoute: RouteLike, options: RequestData = {}) { + public async post(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Post }); } @@ -318,7 +320,7 @@ export class REST extends EventEmitter { * @param fullRoute - The full route to query * @param options - Optional request options */ - public put(fullRoute: RouteLike, options: RequestData = {}) { + public async put(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Put }); } @@ -328,7 +330,7 @@ export class REST extends EventEmitter { * @param fullRoute - The full route to query * @param options - Optional request options */ - public patch(fullRoute: RouteLike, options: RequestData = {}) { + public async patch(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Patch }); } @@ -347,7 +349,7 @@ export class REST extends EventEmitter { * * @param options - Request options */ - public raw(options: InternalRequest) { + public async raw(options: InternalRequest) { return this.requestManager.queueRequest(options); } } diff --git a/packages/rest/src/lib/RequestManager.ts b/packages/rest/src/lib/RequestManager.ts index 3ef43edc01d6..594ad128df40 100644 --- a/packages/rest/src/lib/RequestManager.ts +++ b/packages/rest/src/lib/RequestManager.ts @@ -1,16 +1,20 @@ -import { Blob } from 'node:buffer'; +import { Blob, Buffer } from 'node:buffer'; import { EventEmitter } from 'node:events'; +import { setInterval, clearInterval } from 'node:timers'; +import type { URLSearchParams } from 'node:url'; import { Collection } from '@discordjs/collection'; import { DiscordSnowflake } from '@sapphire/snowflake'; -import { FormData, type RequestInit, type BodyInit, type Dispatcher, Agent } from 'undici'; +import { FormData, type RequestInit, type BodyInit, type Dispatcher, type Agent } from 'undici'; import type { RESTOptions, RestEvents, RequestOptions } from './REST'; import type { IHandler } from './handlers/IHandler'; -import { SequentialHandler } from './handlers/SequentialHandler'; -import { DefaultRestOptions, DefaultUserAgent, RESTEvents } from './utils/constants'; -import { resolveBody } from './utils/utils'; +import { SequentialHandler } from './handlers/SequentialHandler.js'; +import { DefaultRestOptions, DefaultUserAgent, RESTEvents } from './utils/constants.js'; +import { resolveBody } from './utils/utils.js'; // Make this a lazy dynamic import as file-type is a pure ESM package -const getFileType = (): Promise => { +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +const getFileType = async (): Promise => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports let cached: Promise; return (cached ??= import('file-type')); }; @@ -20,9 +24,13 @@ const getFileType = (): Promise => { */ export interface RawFile { /** - * The name of the file + * Content-Type of the file */ - name: string; + contentType?: string; + /** + * The actual data for the file + */ + data: Buffer | boolean | number | string; /** * An explicit key to use for key of the formdata field for this file. * When not provided, the index of the file in the files array is used in the form `files[${index}]`. @@ -30,13 +38,9 @@ export interface RawFile { */ key?: string; /** - * The actual data for the file - */ - data: string | number | boolean | Buffer; - /** - * Content-Type of the file + * The name of the file */ - contentType?: string; + name: string; } /** @@ -58,7 +62,7 @@ export interface RequestData { * * @defaultValue `'Bot'` */ - authPrefix?: 'Bot' | 'Bearer'; + authPrefix?: 'Bearer' | 'Bot'; /** * The body to send to this request. * If providing as BodyInit, set `passThroughBody: true` @@ -125,11 +129,11 @@ export type RouteLike = `/${string}`; * @internal */ export interface InternalRequest extends RequestData { - method: RequestMethod; fullRoute: RouteLike; + method: RequestMethod; } -export type HandlerRequestData = Pick; +export type HandlerRequestData = Pick; /** * Parsed route data for an endpoint @@ -137,8 +141,8 @@ export type HandlerRequestData = Pick(event: K, listener: (...args: RestEvents[K]) => void) => this) & - ((event: Exclude, listener: (...args: any[]) => void) => this); - - once: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & - ((event: Exclude, listener: (...args: any[]) => void) => this); - emit: ((event: K, ...args: RestEvents[K]) => boolean) & ((event: Exclude, ...args: any[]) => boolean); off: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & ((event: Exclude, listener: (...args: any[]) => void) => this); + on: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & + ((event: Exclude, listener: (...args: any[]) => void) => this); + + once: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & + ((event: Exclude, listener: (...args: any[]) => void) => this); + removeAllListeners: ((event?: K) => this) & ((event?: Exclude) => this); } @@ -178,6 +182,7 @@ export class RequestManager extends EventEmitter { * performed by this manager. */ public agent: Dispatcher | null = null; + /** * The number of requests remaining in the global bucket */ @@ -207,6 +212,7 @@ export class RequestManager extends EventEmitter { #token: string | null = null; private hashTimer!: NodeJS.Timer; + private handlerTimer!: NodeJS.Timer; public readonly options: RESTOptions; @@ -223,34 +229,35 @@ export class RequestManager extends EventEmitter { } private setupSweepers() { + // eslint-disable-next-line unicorn/consistent-function-scoping const validateMaxInterval = (interval: number) => { if (interval > 14_400_000) { throw new Error('Cannot set an interval greater than 4 hours'); } }; - if (this.options.hashSweepInterval !== 0 && this.options.hashSweepInterval !== Infinity) { + if (this.options.hashSweepInterval !== 0 && this.options.hashSweepInterval !== Number.POSITIVE_INFINITY) { validateMaxInterval(this.options.hashSweepInterval); this.hashTimer = setInterval(() => { const sweptHashes = new Collection(); const currentDate = Date.now(); // Begin sweeping hash based on lifetimes - this.hashes.sweep((v, k) => { + this.hashes.sweep((val, key) => { // `-1` indicates a global hash - if (v.lastAccess === -1) return false; + if (val.lastAccess === -1) return false; // Check if lifetime has been exceeded - const shouldSweep = Math.floor(currentDate - v.lastAccess) > this.options.hashLifetime; + const shouldSweep = Math.floor(currentDate - val.lastAccess) > this.options.hashLifetime; // Add hash to collection of swept hashes if (shouldSweep) { // Add to swept hashes - sweptHashes.set(k, v); + sweptHashes.set(key, val); } // Emit debug information - this.emit(RESTEvents.Debug, `Hash ${v.value} for ${k} swept due to lifetime being exceeded`); + this.emit(RESTEvents.Debug, `Hash ${val.value} for ${key} swept due to lifetime being exceeded`); return shouldSweep; }); @@ -260,21 +267,21 @@ export class RequestManager extends EventEmitter { }, this.options.hashSweepInterval).unref(); } - if (this.options.handlerSweepInterval !== 0 && this.options.handlerSweepInterval !== Infinity) { + if (this.options.handlerSweepInterval !== 0 && this.options.handlerSweepInterval !== Number.POSITIVE_INFINITY) { validateMaxInterval(this.options.handlerSweepInterval); this.handlerTimer = setInterval(() => { const sweptHandlers = new Collection(); // Begin sweeping handlers based on activity - this.handlers.sweep((v, k) => { - const { inactive } = v; + this.handlers.sweep((val, key) => { + const { inactive } = val; // Collect inactive handlers if (inactive) { - sweptHandlers.set(k, v); + sweptHandlers.set(key, val); } - this.emit(RESTEvents.Debug, `Handler ${v.id} for ${k} swept due to being inactive`); + this.emit(RESTEvents.Debug, `Handler ${val.id} for ${key} swept due to being inactive`); return inactive; }); @@ -308,7 +315,6 @@ export class RequestManager extends EventEmitter { * Queues a request to be sent * * @param request - All the information needed to make a request - * * @returns The response from the api request */ public async queueRequest(request: InternalRequest): Promise { @@ -341,7 +347,6 @@ export class RequestManager extends EventEmitter { * * @param hash - The hash for the route * @param majorParameter - The major parameter for this handler - * * @internal */ private createHandler(hash: string, majorParameter: string) { @@ -358,7 +363,7 @@ export class RequestManager extends EventEmitter { * * @param request - The request data */ - private async resolveRequest(request: InternalRequest): Promise<{ url: string; fetchOptions: RequestOptions }> { + private async resolveRequest(request: InternalRequest): Promise<{ fetchOptions: RequestOptions; url: string }> { const { options } = this; let query = ''; @@ -423,7 +428,7 @@ export class RequestManager extends EventEmitter { } // If a JSON body was added as well, attach it to the form data, using payload_json unless otherwise specified - // eslint-disable-next-line no-eq-null + // eslint-disable-next-line no-eq-null, eqeqeq if (request.body != null) { if (request.appendToFormData) { for (const [key, value] of Object.entries(request.body as Record)) { @@ -437,7 +442,7 @@ export class RequestManager extends EventEmitter { // Set the final body to the form data finalBody = formData; - // eslint-disable-next-line no-eq-null + // eslint-disable-next-line no-eq-null, eqeqeq } else if (request.body != null) { if (request.passThroughBody) { finalBody = request.body as BodyInit; @@ -453,7 +458,7 @@ export class RequestManager extends EventEmitter { const fetchOptions: RequestOptions = { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - headers: { ...(request.headers ?? {}), ...additionalHeaders, ...headers } as Record, + headers: { ...request.headers, ...additionalHeaders, ...headers } as Record, method: request.method.toUpperCase() as Dispatcher.HttpMethod, }; @@ -486,7 +491,6 @@ export class RequestManager extends EventEmitter { * * @param endpoint - The raw endpoint to generalize * @param method - The HTTP method this endpoint is called without - * * @internal */ private static generateRouteData(endpoint: RouteLike, method: RequestMethod): RouteData { @@ -508,7 +512,7 @@ export class RequestManager extends EventEmitter { if (method === RequestMethod.Delete && baseRoute === '/channels/:id/messages/:id') { const id = /\d{16,19}$/.exec(endpoint)![0]!; const timestamp = DiscordSnowflake.timestampFrom(id); - if (Date.now() - timestamp > 1000 * 60 * 60 * 24 * 14) { + if (Date.now() - timestamp > 1_000 * 60 * 60 * 24 * 14) { exceptions += '/Delete Old Message'; } } diff --git a/packages/rest/src/lib/errors/DiscordAPIError.ts b/packages/rest/src/lib/errors/DiscordAPIError.ts index 395516a845ca..54341f3add00 100644 --- a/packages/rest/src/lib/errors/DiscordAPIError.ts +++ b/packages/rest/src/lib/errors/DiscordAPIError.ts @@ -1,4 +1,4 @@ -import type { InternalRequest, RawFile } from '../RequestManager'; +import type { InternalRequest, RawFile } from '../RequestManager.js'; interface DiscordErrorFieldInformation { code: string; @@ -9,12 +9,12 @@ interface DiscordErrorGroupWrapper { _errors: DiscordError[]; } -type DiscordError = DiscordErrorGroupWrapper | DiscordErrorFieldInformation | { [k: string]: DiscordError } | string; +type DiscordError = DiscordErrorFieldInformation | DiscordErrorGroupWrapper | string | { [k: string]: DiscordError }; export interface DiscordErrorData { code: number; - message: string; errors?: DiscordError; + message: string; } export interface OAuthErrorData { @@ -37,7 +37,6 @@ function isErrorResponse(error: DiscordError): error is DiscordErrorFieldInforma /** * Represents an API error returned by Discord - * @extends Error */ export class DiscordAPIError extends Error { public requestBody: RequestBody; @@ -56,7 +55,7 @@ export class DiscordAPIError extends Error { public status: number, public method: string, public url: string, - bodyData: Pick, + bodyData: Pick, ) { super(DiscordAPIError.getMessage(rawError)); @@ -76,31 +75,40 @@ export class DiscordAPIError extends Error { if (error.errors) { flattened = [...this.flattenDiscordError(error.errors)].join('\n'); } + return error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened || 'Unknown Error'; } + return error.error_description ?? 'No Description'; } + // eslint-disable-next-line consistent-return private static *flattenDiscordError(obj: DiscordError, key = ''): IterableIterator { if (isErrorResponse(obj)) { return yield `${key.length ? `${key}[${obj.code}]` : `${obj.code}`}: ${obj.message}`.trim(); } - for (const [k, v] of Object.entries(obj)) { - const nextKey = k.startsWith('_') ? key : key ? (Number.isNaN(Number(k)) ? `${key}.${k}` : `${key}[${k}]`) : k; - - if (typeof v === 'string') { - yield v; + for (const [otherKey, val] of Object.entries(obj)) { + const nextKey = otherKey.startsWith('_') + ? key + : key + ? Number.isNaN(Number(otherKey)) + ? `${key}.${otherKey}` + : `${key}[${otherKey}]` + : otherKey; + + if (typeof val === 'string') { + yield val; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - } else if (isErrorGroupWrapper(v)) { - for (const error of v._errors) { + } else if (isErrorGroupWrapper(val)) { + for (const error of val._errors) { yield* this.flattenDiscordError(error, nextKey); } } else { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - yield* this.flattenDiscordError(v, nextKey); + yield* this.flattenDiscordError(val, nextKey); } } } diff --git a/packages/rest/src/lib/errors/HTTPError.ts b/packages/rest/src/lib/errors/HTTPError.ts index b356bd19111b..14912e77a716 100644 --- a/packages/rest/src/lib/errors/HTTPError.ts +++ b/packages/rest/src/lib/errors/HTTPError.ts @@ -1,5 +1,5 @@ -import type { RequestBody } from './DiscordAPIError'; -import type { InternalRequest } from '../RequestManager'; +import type { InternalRequest } from '../RequestManager.js'; +import type { RequestBody } from './DiscordAPIError.js'; /** * Represents a HTTP error @@ -19,7 +19,7 @@ export class HTTPError extends Error { public status: number, public method: string, public url: string, - bodyData: Pick, + bodyData: Pick, ) { super(); diff --git a/packages/rest/src/lib/errors/RateLimitError.ts b/packages/rest/src/lib/errors/RateLimitError.ts index b7b9dbe72645..7c47fc56c582 100644 --- a/packages/rest/src/lib/errors/RateLimitError.ts +++ b/packages/rest/src/lib/errors/RateLimitError.ts @@ -2,13 +2,21 @@ import type { RateLimitData } from '../REST'; export class RateLimitError extends Error implements RateLimitData { public timeToReset: number; + public limit: number; + public method: string; + public hash: string; + public url: string; + public route: string; + public majorParameter: string; + public global: boolean; + public constructor({ timeToReset, limit, method, hash, url, route, majorParameter, global }: RateLimitData) { super(); this.timeToReset = timeToReset; diff --git a/packages/rest/src/lib/handlers/IHandler.ts b/packages/rest/src/lib/handlers/IHandler.ts index c5370f931ada..8471829f5443 100644 --- a/packages/rest/src/lib/handlers/IHandler.ts +++ b/packages/rest/src/lib/handlers/IHandler.ts @@ -1,8 +1,17 @@ import type { Dispatcher } from 'undici'; import type { RequestOptions } from '../REST'; -import type { HandlerRequestData, RouteData } from '../RequestManager'; +import type { HandlerRequestData, RouteData } from '../RequestManager.js'; export interface IHandler { + /** + * The unique id of the handler + */ + readonly id: string; + /** + * If the bucket is currently inactive (no pending requests) + */ + // eslint-disable-next-line @typescript-eslint/method-signature-style -- This is meant to be a getter returning a bool + get inactive(): boolean; /** * Queues a request to be sent * @@ -11,19 +20,10 @@ export interface IHandler { * @param options - All the information needed to make a request * @param requestData - Extra data from the user's request needed for errors and additional processing */ - queueRequest: ( + queueRequest( routeId: RouteData, url: string, options: RequestOptions, requestData: HandlerRequestData, - ) => Promise; - /** - * If the bucket is currently inactive (no pending requests) - */ - // eslint-disable-next-line @typescript-eslint/method-signature-style -- This is meant to be a getter returning a bool - get inactive(): boolean; - /** - * The unique id of the handler - */ - readonly id: string; + ): Promise; } diff --git a/packages/rest/src/lib/handlers/SequentialHandler.ts b/packages/rest/src/lib/handlers/SequentialHandler.ts index b53b8ed8cd37..bb5447ea0e10 100644 --- a/packages/rest/src/lib/handlers/SequentialHandler.ts +++ b/packages/rest/src/lib/handlers/SequentialHandler.ts @@ -1,14 +1,15 @@ +import { setTimeout, clearTimeout } from 'node:timers'; import { setTimeout as sleep } from 'node:timers/promises'; import { AsyncQueue } from '@sapphire/async-queue'; import { request, type Dispatcher } from 'undici'; -import type { IHandler } from './IHandler'; import type { RateLimitData, RequestOptions } from '../REST'; import type { HandlerRequestData, RequestManager, RouteData } from '../RequestManager'; -import { DiscordAPIError, DiscordErrorData, OAuthErrorData } from '../errors/DiscordAPIError'; -import { HTTPError } from '../errors/HTTPError'; -import { RateLimitError } from '../errors/RateLimitError'; -import { RESTEvents } from '../utils/constants'; -import { hasSublimit, parseHeader, parseResponse } from '../utils/utils'; +import { DiscordAPIError, type DiscordErrorData, type OAuthErrorData } from '../errors/DiscordAPIError.js'; +import { HTTPError } from '../errors/HTTPError.js'; +import { RateLimitError } from '../errors/RateLimitError.js'; +import { RESTEvents } from '../utils/constants.js'; +import { hasSublimit, parseHeader, parseResponse } from '../utils/utils.js'; +import type { IHandler } from './IHandler.js'; /** * Invalid request limiting is done on a per-IP basis, not a per-token basis. @@ -47,7 +48,7 @@ export class SequentialHandler implements IHandler { /** * The total number of requests that can be made before we are rate limited */ - private limit = Infinity; + private limit = Number.POSITIVE_INFINITY; /** * The interface used to sequence async requests sequentially @@ -65,7 +66,7 @@ export class SequentialHandler implements IHandler { * A promise wrapper for when the sublimited queue is finished being processed or null when not being processed */ // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - #sublimitPromise: { promise: Promise; resolve: () => void } | null = null; + #sublimitPromise: { promise: Promise; resolve(): void } | null = null; /** * Whether the sublimit queue needs to be shifted in the finally block @@ -176,6 +177,7 @@ export class SequentialHandler implements IHandler { queue = this.#sublimitedQueue!; queueType = QueueType.Sublimit; } + // Wait for any previous requests to be completed before this one is run await queue.wait(); // This set handles retroactively sublimiting requests @@ -194,6 +196,7 @@ export class SequentialHandler implements IHandler { await this.#sublimitPromise.promise; } } + try { // Make the request, and return the results return await this.runRequest(routeId, url, options, requestData); @@ -204,6 +207,7 @@ export class SequentialHandler implements IHandler { this.#shiftSublimit = false; this.#sublimitedQueue?.shift(); } + // If this request is the last request in a sublimit if (this.#sublimitedQueue?.remaining === 0) { this.#sublimitPromise?.resolve(); @@ -247,6 +251,7 @@ export class SequentialHandler implements IHandler { // The global delay function clears the global delay state when it is resolved this.manager.globalDelay = this.globalDelayFor(timeout); } + delay = this.manager.globalDelay; } else { // Set RateLimitData based on the route-specific limit @@ -254,6 +259,7 @@ export class SequentialHandler implements IHandler { timeout = this.timeToReset; delay = sleep(timeout); } + const rateLimitData: RateLimitData = { timeToReset: timeout, limit, @@ -274,14 +280,17 @@ export class SequentialHandler implements IHandler { } else { this.debug(`Waiting ${timeout}ms for rate limit to pass`); } + // Wait the remaining time left before the rate limit resets await delay; } + // As the request goes out, update the global usage information if (!this.manager.globalReset || this.manager.globalReset < Date.now()) { - this.manager.globalReset = Date.now() + 1000; + this.manager.globalReset = Date.now() + 1_000; this.manager.globalRemaining = this.manager.options.globalRequestsPerSecond; } + this.manager.globalRemaining--; const method = options.method ?? 'get'; @@ -295,6 +304,7 @@ export class SequentialHandler implements IHandler { } catch (error: unknown) { // Retry the specified number of times for possible timed out requests if (error instanceof Error && error.name === 'AbortError' && retries !== this.manager.options.retries) { + // eslint-disable-next-line no-param-reassign return await this.runRequest(routeId, url, options, requestData, ++retries); } @@ -328,14 +338,14 @@ export class SequentialHandler implements IHandler { const retry = parseHeader(res.headers['retry-after']); // Update the total number of requests that can be made before the rate limit resets - this.limit = limit ? Number(limit) : Infinity; + this.limit = limit ? Number(limit) : Number.POSITIVE_INFINITY; // Update the number of remaining requests that can be made before the rate limit resets this.remaining = remaining ? Number(remaining) : 1; // Update the time when this rate limit resets (reset-after is in seconds) - this.reset = reset ? Number(reset) * 1000 + Date.now() + this.manager.options.offset : Date.now(); + this.reset = reset ? Number(reset) * 1_000 + Date.now() + this.manager.options.offset : Date.now(); // Amount of time in milliseconds until we should retry if rate limited (globally or otherwise) - if (retry) retryAfter = Number(retry) * 1000 + this.manager.options.offset; + if (retry) retryAfter = Number(retry) * 1_000 + this.manager.options.offset; // Handle buckets via the hash header retroactively if (hash && hash !== this.hash) { @@ -373,9 +383,10 @@ export class SequentialHandler implements IHandler { // Count the invalid requests if (status === 401 || status === 403 || status === 429) { if (!invalidCountResetTime || invalidCountResetTime < Date.now()) { - invalidCountResetTime = Date.now() + 1000 * 60 * 10; + invalidCountResetTime = Date.now() + 1_000 * 60 * 10; invalidCount = 0; } + invalidCount++; const emitInvalid = @@ -407,6 +418,7 @@ export class SequentialHandler implements IHandler { limit = this.limit; timeout = this.timeToReset; } + await this.onRateLimit({ timeToReset: timeout, limit, @@ -440,10 +452,12 @@ export class SequentialHandler implements IHandler { void this.#sublimitedQueue.wait(); this.#asyncQueue.shift(); } + this.#sublimitPromise?.resolve(); this.#sublimitPromise = null; await sleep(sublimitTimeout, undefined, { ref: false }); let resolve: () => void; + // eslint-disable-next-line promise/param-names, no-promise-executor-return const promise = new Promise((res) => (resolve = res)); this.#sublimitPromise = { promise, resolve: resolve! }; if (firstSublimit) { @@ -452,13 +466,16 @@ export class SequentialHandler implements IHandler { this.#shiftSublimit = true; } } + // Since this is not a server side issue, the next request should pass, so we don't bump the retries counter return this.runRequest(routeId, url, options, requestData, retries); } else if (status >= 500 && status < 600) { // Retry the specified number of times for possible server side issues if (retries !== this.manager.options.retries) { + // eslint-disable-next-line no-param-reassign return this.runRequest(routeId, url, options, requestData, ++retries); } + // We are out of retries, throw an error throw new HTTPError(res.constructor.name, status, method, url, requestData); } else { @@ -468,11 +485,13 @@ export class SequentialHandler implements IHandler { if (status === 401 && requestData.auth) { this.manager.setToken(null!); } + // The request will not succeed for some reason, parse the error returned from the api const data = (await parseResponse(res)) as DiscordErrorData | OAuthErrorData; // throw the API error throw new DiscordAPIError(data, 'code' in data ? data.code : data.error, status, method, url, requestData); } + return res; } } diff --git a/packages/rest/src/lib/utils/constants.ts b/packages/rest/src/lib/utils/constants.ts index ee6f4a74ceb1..2c5106572b2a 100644 --- a/packages/rest/src/lib/utils/constants.ts +++ b/packages/rest/src/lib/utils/constants.ts @@ -1,6 +1,7 @@ +import process from 'node:process'; import { APIVersion } from 'discord-api-types/v10'; import { getGlobalDispatcher } from 'undici'; -import type { RESTOptions } from '../REST'; +import type { RESTOptions } from '../REST.js'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment const Package = require('../../../package.json'); @@ -33,16 +34,16 @@ export const DefaultRestOptions: Required = { */ export const enum RESTEvents { Debug = 'restDebug', + HandlerSweep = 'handlerSweep', + HashSweep = 'hashSweep', InvalidRequestWarning = 'invalidRequestWarning', RateLimited = 'rateLimited', Response = 'response', - HashSweep = 'hashSweep', - HandlerSweep = 'handlerSweep', } export const ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'] as const; export const ALLOWED_STICKER_EXTENSIONS = ['png', 'json'] as const; -export const ALLOWED_SIZES = [16, 32, 64, 128, 256, 512, 1024, 2048, 4096] as const; +export const ALLOWED_SIZES = [16, 32, 64, 128, 256, 512, 1_024, 2_048, 4_096] as const; export type ImageExtension = typeof ALLOWED_EXTENSIONS[number]; export type StickerExtension = typeof ALLOWED_STICKER_EXTENSIONS[number]; diff --git a/packages/rest/src/lib/utils/utils.ts b/packages/rest/src/lib/utils/utils.ts index 1b437ecd3fbe..3cc69a5c8efc 100644 --- a/packages/rest/src/lib/utils/utils.ts +++ b/packages/rest/src/lib/utils/utils.ts @@ -1,12 +1,12 @@ -import { Blob } from 'node:buffer'; +import { Blob, Buffer } from 'node:buffer'; import { URLSearchParams } from 'node:url'; import { types } from 'node:util'; import type { RESTPatchAPIChannelJSONBody } from 'discord-api-types/v10'; import { FormData, type Dispatcher, type RequestInit } from 'undici'; -import type { RequestOptions } from '../REST'; -import { RequestMethod } from '../RequestManager'; +import type { RequestOptions } from '../REST.js'; +import { RequestMethod } from '../RequestManager.js'; -export function parseHeader(header: string | string[] | undefined): string | undefined { +export function parseHeader(header: string[] | string | undefined): string | undefined { if (header === undefined) { return header; } else if (typeof header === 'string') { @@ -29,6 +29,7 @@ function serializeSearchParam(value: unknown): string | null { if (value instanceof Date) { return Number.isNaN(value.getTime()) ? null : value.toISOString(); } + // eslint-disable-next-line @typescript-eslint/no-base-to-string if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) return value.toString(); return null; @@ -42,7 +43,6 @@ function serializeSearchParam(value: unknown): string | null { * out null and undefined values, while also coercing non-strings to strings. * * @param options - The options to use - * * @returns A populated URLSearchParams instance */ export function makeURLSearchParams(options?: Record) { @@ -62,7 +62,7 @@ export function makeURLSearchParams(options?: Record) { * * @param res - The fetch response */ -export function parseResponse(res: Dispatcher.ResponseData): Promise { +export async function parseResponse(res: Dispatcher.ResponseData): Promise { const header = parseHeader(res.headers['content-type']); if (header?.startsWith('application/json')) { return res.body.json(); @@ -77,7 +77,6 @@ export function parseResponse(res: Dispatcher.ResponseData): Promise { * @param bucketRoute - The buckets route identifier * @param body - The options provided as JSON data * @param method - The HTTP method that will be used to make the request - * * @returns Whether the request falls under a sublimit */ export function hasSublimit(bucketRoute: string, body?: unknown, method?: string): boolean { @@ -97,7 +96,7 @@ export function hasSublimit(bucketRoute: string, body?: unknown, method?: string } export async function resolveBody(body: RequestInit['body']): Promise { - // eslint-disable-next-line no-eq-null + // eslint-disable-next-line no-eq-null, eqeqeq if (body == null) { return null; } else if (typeof body === 'string') { diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 44522517dd69..3311fa02dd29 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -51,14 +51,9 @@ }, "devDependencies": { "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@vitest/coverage-c8": "^0.22.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", + "eslint-config-neon": "^0.1.23", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", "typescript": "^4.8.2", diff --git a/packages/scripts/src/generateIndex.ts b/packages/scripts/src/generateIndex.ts index 3231fc3d14d0..95b6e197f743 100644 --- a/packages/scripts/src/generateIndex.ts +++ b/packages/scripts/src/generateIndex.ts @@ -1,18 +1,19 @@ import { stat, mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { generatePath } from '@discordjs/api-extractor-utils'; -import { ApiDeclaredItem, ApiItem, ApiItemContainerMixin, ApiModel } from '@microsoft/api-extractor-model'; -import { DocCodeSpan, DocNode, DocNodeKind, DocParagraph, DocPlainText } from '@microsoft/tsdoc'; +import { ApiDeclaredItem, ApiItemContainerMixin, type ApiItem, type ApiModel } from '@microsoft/api-extractor-model'; +import { DocNodeKind, type DocCodeSpan, type DocNode, type DocParagraph, type DocPlainText } from '@microsoft/tsdoc'; export interface MemberJSON { - name: string; kind: string; - summary: string | null; + name: string; path: string; + summary: string | null; } /** * Attempts to resolve the summary text for the given item. + * * @param item - The API item to resolve the summary text for. */ function tryResolveSummaryText(item: ApiDeclaredItem): string | null { @@ -34,8 +35,14 @@ function tryResolveSummaryText(item: ApiDeclaredItem): string | null { retVal += (node as DocPlainText).text; break; case DocNodeKind.Section: - case DocNodeKind.Paragraph: - return (node as DocParagraph).nodes.forEach(visitTSDocNode); + case DocNodeKind.Paragraph: { + for (const child of (node as DocParagraph).nodes) { + visitTSDocNode(child); + } + + break; + } + default: // We'll ignore all other nodes. break; } diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts index 1b4fba204387..353651a35f6b 100644 --- a/packages/scripts/src/index.ts +++ b/packages/scripts/src/index.ts @@ -1 +1 @@ -export * from './generateIndex'; +export * from './generateIndex.js'; diff --git a/packages/voice/.eslintrc.json b/packages/voice/.eslintrc.json index e63740acf414..f2acef1280ab 100644 --- a/packages/voice/.eslintrc.json +++ b/packages/voice/.eslintrc.json @@ -2,7 +2,8 @@ "extends": "../../.eslintrc.json", "plugins": ["eslint-plugin-tsdoc"], "rules": { - "tsdoc/syntax": "warn" + "import/extensions": 0, + "no-restricted-globals": 0 }, "parserOptions": { "project": "./tsconfig.eslint.json", diff --git a/packages/voice/__tests__/AudioPlayer.test.ts b/packages/voice/__tests__/AudioPlayer.test.ts index 83306c2f9ac9..544b2526278b 100644 --- a/packages/voice/__tests__/AudioPlayer.test.ts +++ b/packages/voice/__tests__/AudioPlayer.test.ts @@ -1,14 +1,17 @@ +/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/dot-notation */ +import { Buffer } from 'node:buffer'; import { once } from 'node:events'; +import process from 'node:process'; import { Readable } from 'node:stream'; -import { NoSubscriberBehavior } from '../src'; import { addAudioPlayer, deleteAudioPlayer } from '../src/DataStore'; import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection'; -import { createAudioPlayer, AudioPlayerStatus, AudioPlayer, SILENCE_FRAME } from '../src/audio/AudioPlayer'; +import { createAudioPlayer, AudioPlayerStatus, SILENCE_FRAME, type AudioPlayerState } from '../src/audio/AudioPlayer'; import { AudioPlayerError } from '../src/audio/AudioPlayerError'; import { AudioResource } from '../src/audio/AudioResource'; +import { NoSubscriberBehavior } from '../src/index'; jest.mock('../src/DataStore'); jest.mock('../src/VoiceConnection'); @@ -39,7 +42,8 @@ function createVoiceConnectionMock() { return connection; } -function wait() { +async function wait() { + // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => process.nextTick(resolve)); } @@ -47,6 +51,7 @@ async function started(resource: AudioResource) { while (!resource.started) { await wait(); } + return resource; } @@ -197,6 +202,7 @@ describe('State transitions', () => { if (connection.state.status !== VoiceConnectionStatus.Signalling) { throw new Error('Voice connection should have been Signalling'); } + connection.state = { ...connection.state, status: VoiceConnectionStatus.Ready, @@ -216,18 +222,18 @@ describe('State transitions', () => { expect(player.checkPlayable()).toEqual(true); // Run through a few packet cycles - for (let i = 1; i <= 5; i++) { + for (let index = 1; index <= 5; index++) { player['_stepDispatch'](); - expect(connection.dispatchAudio).toHaveBeenCalledTimes(i); + expect(connection.dispatchAudio).toHaveBeenCalledTimes(index); await wait(); // Wait for the stream player['_stepPrepare'](); - expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(i); + expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(index); expect(connection.prepareAudioPacket).toHaveBeenLastCalledWith(buffer); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); if (player.state.status === AudioPlayerStatus.Playing) { - expect(player.state.playbackDuration).toStrictEqual(i * 20); + expect(player.state.playbackDuration).toStrictEqual(index * 20); } } @@ -254,6 +260,7 @@ describe('State transitions', () => { if (connection.state.status !== VoiceConnectionStatus.Signalling) { throw new Error('Voice connection should have been Signalling'); } + connection.state = { ...connection.state, status: VoiceConnectionStatus.Ready, @@ -275,20 +282,21 @@ describe('State transitions', () => { player.stop(); // Run through a few packet cycles - for (let i = 1; i <= 5; i++) { + for (let index = 1; index <= 5; index++) { player['_stepDispatch'](); - expect(connection.dispatchAudio).toHaveBeenCalledTimes(i); + expect(connection.dispatchAudio).toHaveBeenCalledTimes(index); await wait(); // Wait for the stream player['_stepPrepare'](); - expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(i); + expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(index); expect(connection.prepareAudioPacket).toHaveBeenLastCalledWith(SILENCE_FRAME); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); if (player.state.status === AudioPlayerStatus.Playing) { - expect(player.state.playbackDuration).toStrictEqual(i * 20); + expect(player.state.playbackDuration).toStrictEqual(index * 20); } } + await wait(); expect(player.checkPlayable()).toEqual(false); const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock< @@ -307,6 +315,7 @@ describe('State transitions', () => { if (connection.state.status !== VoiceConnectionStatus.Signalling) { throw new Error('Voice connection should have been Signalling'); } + connection.state = { ...connection.state, status: VoiceConnectionStatus.Ready, @@ -328,16 +337,16 @@ describe('State transitions', () => { >; // Run through a few packet cycles - for (let i = 1; i <= 5; i++) { + for (let index = 1; index <= 5; index++) { expect(player.state.status).toEqual(AudioPlayerStatus.Playing); if (player.state.status !== AudioPlayerStatus.Playing) throw new Error('Error'); - expect(player.state.playbackDuration).toStrictEqual((i - 1) * 20); - expect(player.state.missedFrames).toEqual(i - 1); + expect(player.state.playbackDuration).toStrictEqual((index - 1) * 20); + expect(player.state.missedFrames).toEqual(index - 1); player['_stepDispatch'](); - expect(connection.dispatchAudio).toHaveBeenCalledTimes(i); + expect(connection.dispatchAudio).toHaveBeenCalledTimes(index); player['_stepPrepare'](); - expect(prepareAudioPacket).toHaveBeenCalledTimes(i); - expect(prepareAudioPacket.mock.calls[i - 1][0]).toEqual(silence().next().value); + expect(prepareAudioPacket).toHaveBeenCalledTimes(index); + expect(prepareAudioPacket.mock.calls[index - 1][0]).toEqual(silence().next().value); } expect(player.state.status).toEqual(AudioPlayerStatus.Idle); @@ -352,10 +361,11 @@ describe('State transitions', () => { player.play(resource); expect(player.checkPlayable()).toEqual(true); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - for (let i = 0; i < 3; i++) { + for (let index = 0; index < 3; index++) { resource.playStream.read(); await wait(); } + expect(resource.playStream.readableEnded).toEqual(true); expect(player.checkPlayable()).toEqual(false); expect(player.state.status).toEqual(AudioPlayerStatus.Idle); @@ -367,10 +377,11 @@ test('play() throws when playing a resource that has already ended', async () => player = createAudioPlayer(); player.play(resource); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - for (let i = 0; i < 3; i++) { + for (let index = 0; index < 3; index++) { resource.playStream.read(); await wait(); } + expect(resource.playStream.readableEnded).toEqual(true); player.stop(true); expect(player.state.status).toEqual(AudioPlayerStatus.Idle); diff --git a/packages/voice/__tests__/AudioReceiveStream.test.ts b/packages/voice/__tests__/AudioReceiveStream.test.ts index 75a70cf120a1..415ec1bf2b69 100644 --- a/packages/voice/__tests__/AudioReceiveStream.test.ts +++ b/packages/voice/__tests__/AudioReceiveStream.test.ts @@ -1,9 +1,11 @@ +/* eslint-disable no-promise-executor-return */ +import { Buffer } from 'node:buffer'; import { SILENCE_FRAME } from '../src/audio/AudioPlayer'; import { AudioReceiveStream, EndBehaviorType } from '../src/receive/AudioReceiveStream'; const DUMMY_BUFFER = Buffer.allocUnsafe(16); -function wait(ms: number) { +async function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -52,13 +54,13 @@ describe('AudioReceiveStream', () => { const stream = new AudioReceiveStream({ end: { behavior: EndBehaviorType.AfterInactivity, duration: 100 } }); stream.resume(); - for (let i = increment; i < duration / 2; i += increment) { + for (let index = increment; index < duration / 2; index += increment) { await stepSilence(stream, increment); } stream.push(DUMMY_BUFFER); - for (let i = increment; i < duration; i += increment) { + for (let index = increment; index < duration; index += increment) { await stepSilence(stream, increment); } diff --git a/packages/voice/__tests__/AudioResource.test.ts b/packages/voice/__tests__/AudioResource.test.ts index 08f3bc1fced8..9a1bb2872ca4 100644 --- a/packages/voice/__tests__/AudioResource.test.ts +++ b/packages/voice/__tests__/AudioResource.test.ts @@ -1,14 +1,17 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ +import { Buffer } from 'node:buffer'; +import process from 'node:process'; import { PassThrough, Readable } from 'node:stream'; import { opus, VolumeTransformer } from 'prism-media'; import { SILENCE_FRAME } from '../src/audio/AudioPlayer'; import { AudioResource, createAudioResource, NO_CONSTRAINT, VOLUME_CONSTRAINT } from '../src/audio/AudioResource'; -import { Edge, findPipeline as _findPipeline, StreamType, TransformerType } from '../src/audio/TransformerGraph'; +import { findPipeline as _findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph'; jest.mock('prism-media'); jest.mock('../src/audio/TransformerGraph'); -function wait() { +async function wait() { + // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => process.nextTick(resolve)); } @@ -16,12 +19,14 @@ async function started(resource: AudioResource) { while (!resource.started) { await wait(); } + return resource; } const findPipeline = _findPipeline as unknown as jest.MockedFunction; beforeAll(() => { + // @ts-expect-error no type findPipeline.mockImplementation((from: StreamType, constraint: (path: Edge[]) => boolean) => { const base = [ { @@ -38,6 +43,7 @@ beforeAll(() => { type: TransformerType.InlineVolume, }); } + return base as any[]; }); }); @@ -113,11 +119,12 @@ describe('createAudioResource', () => { await started(resource); expect(resource.readable).toEqual(true); expect(resource.read()).toEqual(Buffer.from([1])); - for (let i = 0; i < 5; i++) { + for (let index = 0; index < 5; index++) { await wait(); expect(resource.readable).toEqual(true); expect(resource.read()).toEqual(SILENCE_FRAME); } + await wait(); expect(resource.readable).toEqual(false); expect(resource.read()).toEqual(null); diff --git a/packages/voice/__tests__/DataStore.test.ts b/packages/voice/__tests__/DataStore.test.ts index 60cb62659424..78930d46bf0e 100644 --- a/packages/voice/__tests__/DataStore.test.ts +++ b/packages/voice/__tests__/DataStore.test.ts @@ -2,8 +2,9 @@ /* eslint-disable @typescript-eslint/dot-notation */ import { GatewayOpcodes } from 'discord-api-types/v10'; import * as DataStore from '../src/DataStore'; -import { VoiceConnection } from '../src/VoiceConnection'; +import type { VoiceConnection } from '../src/VoiceConnection'; import * as _AudioPlayer from '../src/audio/AudioPlayer'; + jest.mock('../src/VoiceConnection'); jest.mock('../src/audio/AudioPlayer'); @@ -15,8 +16,9 @@ function createVoiceConnection(joinConfig: Pick setImmediate(res)); +async function waitForEventLoop() { + // eslint-disable-next-line no-promise-executor-return + return new Promise((resolve) => setImmediate(resolve)); } beforeEach(() => { @@ -24,6 +26,7 @@ beforeEach(() => { for (const groupKey of groups.keys()) { groups.delete(groupKey); } + groups.set('default', new Map()); }); @@ -41,6 +44,7 @@ describe('DataStore', () => { }; expect(DataStore.createJoinVoiceChannelPayload(joinConfig)).toStrictEqual({ op: GatewayOpcodes.VoiceStateUpdate, + // eslint-disable-next-line id-length d: { guild_id: joinConfig.guildId, channel_id: joinConfig.channelId, @@ -60,7 +64,7 @@ describe('DataStore', () => { expect([...DataStore.getVoiceConnections().values()]).toEqual([voiceConnectionDefault]); expect([...DataStore.getVoiceConnections('default').values()]).toEqual([voiceConnectionDefault]); - expect([...DataStore.getVoiceConnections('abc').values()]).toEqual([voiceConnectionAbc]); + expect([...DataStore.getVoiceConnections('abc')!.values()]).toEqual([voiceConnectionAbc]); DataStore.untrackVoiceConnection(voiceConnectionDefault); expect(DataStore.getVoiceConnection('123')).toBeUndefined(); @@ -73,6 +77,7 @@ describe('DataStore', () => { expect(DataStore.hasAudioPlayer(player)).toEqual(true); expect(DataStore.addAudioPlayer(player)).toEqual(player); DataStore.deleteAudioPlayer(player); + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression expect(DataStore.deleteAudioPlayer(player)).toBeUndefined(); expect(DataStore.hasAudioPlayer(player)).toEqual(false); // Tests audio cycle with nextTime === -1 diff --git a/packages/voice/__tests__/SSRCMap.test.ts b/packages/voice/__tests__/SSRCMap.test.ts index 06d9797c94f9..1da3f26e8b8e 100644 --- a/packages/voice/__tests__/SSRCMap.test.ts +++ b/packages/voice/__tests__/SSRCMap.test.ts @@ -1,8 +1,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import EventEmitter, { once } from 'node:events'; -import { SSRCMap, VoiceUserData } from '../src/receive/SSRCMap'; +import type EventEmitter from 'node:events'; +import { once } from 'node:events'; +import process from 'node:process'; +import { SSRCMap, type VoiceUserData } from '../src/receive/SSRCMap'; -function onceOrThrow(target: T, event: string, after: number) { +async function onceOrThrow(target: T, event: string, after: number) { return new Promise((resolve, reject) => { target.on(event, resolve); setTimeout(() => reject(new Error('Time up')), after); diff --git a/packages/voice/__tests__/Secretbox.test.ts b/packages/voice/__tests__/Secretbox.test.ts index 9e09b33ef6a6..a01dd2f3bf69 100644 --- a/packages/voice/__tests__/Secretbox.test.ts +++ b/packages/voice/__tests__/Secretbox.test.ts @@ -3,6 +3,6 @@ import { methods } from '../src/util/Secretbox'; jest.mock('tweetnacl'); test('Does not throw error with a package installed', () => { - // @ts-expect-error + // @ts-expect-error: Unknown type expect(() => methods.open()).not.toThrowError(); }); diff --git a/packages/voice/__tests__/SpeakingMap.test.ts b/packages/voice/__tests__/SpeakingMap.test.ts index 92087ebd32fa..1a7bb9cc90fc 100644 --- a/packages/voice/__tests__/SpeakingMap.test.ts +++ b/packages/voice/__tests__/SpeakingMap.test.ts @@ -14,7 +14,7 @@ describe('SpeakingMap', () => { speaking.on('start', (userId) => void starts.push(userId)); speaking.on('end', (userId) => void ends.push(userId)); - for (let i = 0; i < 10; i++) { + for (let index = 0; index < 10; index++) { speaking.onPacket(userId); setTimeout(noop, SpeakingMap.DELAY / 2); jest.advanceTimersToNextTimer(); @@ -22,6 +22,7 @@ describe('SpeakingMap', () => { expect(starts).toEqual([userId]); expect(ends).toEqual([]); } + jest.advanceTimersToNextTimer(); expect(ends).toEqual([userId]); diff --git a/packages/voice/__tests__/TransformerGraph.test.ts b/packages/voice/__tests__/TransformerGraph.test.ts index 9860d656e941..3a887381dd50 100644 --- a/packages/voice/__tests__/TransformerGraph.test.ts +++ b/packages/voice/__tests__/TransformerGraph.test.ts @@ -1,4 +1,4 @@ -import { Edge, findPipeline, StreamType, TransformerType } from '../src/audio/TransformerGraph'; +import { findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph'; const noConstraint = () => true; @@ -12,6 +12,7 @@ function reducePath(pipeline: Edge[]) { for (const edge of pipeline.slice(1)) { streams.push(edge.from.type); } + streams.push(pipeline[pipeline.length - 1].to.type); return streams; } diff --git a/packages/voice/__tests__/VoiceConnection.test.ts b/packages/voice/__tests__/VoiceConnection.test.ts index fbff02d3f2ed..c6f0a2e80c4b 100644 --- a/packages/voice/__tests__/VoiceConnection.test.ts +++ b/packages/voice/__tests__/VoiceConnection.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -8,13 +9,12 @@ import * as _DataStore from '../src/DataStore'; import { createVoiceConnection, VoiceConnection, - VoiceConnectionConnectingState, VoiceConnectionDisconnectReason, - VoiceConnectionReadyState, - VoiceConnectionSignallingState, VoiceConnectionStatus, + type VoiceConnectionConnectingState, + type VoiceConnectionReadyState, + type VoiceConnectionSignallingState, } from '../src/VoiceConnection'; - import * as _AudioPlayer from '../src/audio/AudioPlayer'; import { PlayerSubscription as _PlayerSubscription } from '../src/audio/PlayerSubscription'; import * as _Networking from '../src/networking/Networking'; @@ -129,6 +129,7 @@ describe('createVoiceConnection', () => { const stateSetter = jest.spyOn(existingVoiceConnection, 'state', 'set'); + // @ts-expect-error: We're testing DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') => guildId === existingJoinConfig.guildId && group === existingJoinConfig.group ? existingVoiceConnection : null, ); @@ -167,6 +168,7 @@ describe('createVoiceConnection', () => { const rejoinSpy = jest.spyOn(existingVoiceConnection, 'rejoin'); + // @ts-expect-error: We're testing DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') => guildId === existingJoinConfig.guildId && group === existingJoinConfig.group ? existingVoiceConnection : null, ); @@ -198,6 +200,7 @@ describe('createVoiceConnection', () => { adapterCreator: existingAdapter.creator, }); + // @ts-expect-error: We're testing DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') => guildId === existingJoinConfig.guildId && group === existingJoinConfig.group ? existingVoiceConnection : null, ); @@ -355,17 +358,17 @@ describe('VoiceConnection#onNetworkingClose', () => { voiceConnection.state = { status: VoiceConnectionStatus.Destroyed, }; - voiceConnection['onNetworkingClose'](1000); + voiceConnection['onNetworkingClose'](1_000); expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed); expect(adapter.sendPayload).not.toHaveBeenCalled(); }); test('Disconnects for code 4014', () => { const { voiceConnection, adapter } = createFakeVoiceConnection(); - voiceConnection['onNetworkingClose'](4014); + voiceConnection['onNetworkingClose'](4_014); expect(voiceConnection.state).toMatchObject({ status: VoiceConnectionStatus.Disconnected, - closeCode: 4014, + closeCode: 4_014, }); expect(adapter.sendPayload).not.toHaveBeenCalled(); }); @@ -376,7 +379,7 @@ describe('VoiceConnection#onNetworkingClose', () => { DataStore.createJoinVoiceChannelPayload.mockImplementation((config) => config === joinConfig ? dummyPayload : undefined, ); - voiceConnection['onNetworkingClose'](1234); + voiceConnection['onNetworkingClose'](1_234); expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling); expect(adapter.sendPayload).toHaveBeenCalledWith(dummyPayload); expect(voiceConnection.rejoinAttempts).toEqual(1); @@ -389,7 +392,7 @@ describe('VoiceConnection#onNetworkingClose', () => { config === joinConfig ? dummyPayload : undefined, ); adapter.sendPayload.mockReturnValue(false); - voiceConnection['onNetworkingClose'](1234); + voiceConnection['onNetworkingClose'](1_234); expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Disconnected); expect(adapter.sendPayload).toHaveBeenCalledWith(dummyPayload); expect(voiceConnection.rejoinAttempts).toEqual(1); @@ -552,7 +555,7 @@ describe('VoiceConnection#rejoin', () => { ...(voiceConnection.state as VoiceConnectionSignallingState), status: VoiceConnectionStatus.Disconnected, reason: VoiceConnectionDisconnectReason.WebSocketClose, - closeCode: 1000, + closeCode: 1_000, }; expect(voiceConnection.rejoin()).toEqual(true); expect(voiceConnection.rejoinAttempts).toEqual(1); @@ -584,7 +587,7 @@ describe('VoiceConnection#rejoin', () => { ...(voiceConnection.state as VoiceConnectionSignallingState), status: VoiceConnectionStatus.Disconnected, reason: VoiceConnectionDisconnectReason.WebSocketClose, - closeCode: 1000, + closeCode: 1_000, }; adapter.sendPayload.mockReturnValue(false); expect(voiceConnection.rejoin()).toEqual(false); @@ -751,7 +754,7 @@ describe('Adapter', () => { const { adapter, voiceConnection } = createFakeVoiceConnection(); voiceConnection['addServerPacket'] = jest.fn(); const dummy = Symbol('dummy') as any; - adapter.libMethods.onVoiceServerUpdate(dummy); + adapter.libMethods.onVoiceServerUpdate!(dummy); expect(voiceConnection['addServerPacket']).toHaveBeenCalledWith(dummy); }); @@ -759,13 +762,13 @@ describe('Adapter', () => { const { adapter, voiceConnection } = createFakeVoiceConnection(); voiceConnection['addStatePacket'] = jest.fn(); const dummy = Symbol('dummy') as any; - adapter.libMethods.onVoiceStateUpdate(dummy); + adapter.libMethods.onVoiceStateUpdate!(dummy); expect(voiceConnection['addStatePacket']).toHaveBeenCalledWith(dummy); }); test('destroy', () => { const { adapter, voiceConnection } = createFakeVoiceConnection(); - adapter.libMethods.destroy(); + adapter.libMethods.destroy!(); expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed); expect(adapter.sendPayload).not.toHaveBeenCalled(); }); diff --git a/packages/voice/__tests__/VoiceReceiver.test.ts b/packages/voice/__tests__/VoiceReceiver.test.ts index fd7bcfb70c15..534911b68ad2 100644 --- a/packages/voice/__tests__/VoiceReceiver.test.ts +++ b/packages/voice/__tests__/VoiceReceiver.test.ts @@ -1,6 +1,9 @@ +/* eslint-disable id-length */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/dot-notation */ +import { Buffer } from 'node:buffer'; import { once } from 'node:events'; +import process from 'node:process'; import { VoiceOpcodes } from 'discord-api-types/voice/v4'; import { RTP_PACKET_DESKTOP, RTP_PACKET_CHROME, RTP_PACKET_ANDROID } from '../__mocks__/rtp'; import { VoiceConnection as _VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection'; @@ -16,7 +19,8 @@ openSpy.mockImplementation((buffer) => buffer); const VoiceConnection = _VoiceConnection as unknown as jest.Mocked; -function nextTick() { +async function nextTick() { + // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => process.nextTick(resolve)); } @@ -178,7 +182,7 @@ describe('VoiceReceiver', () => { // Assert expect(nonce.equals(range(29, 32))).toEqual(true); - expect(decrypted.equals(range(13, 28))).toEqual(true); + expect(decrypted!.equals(range(13, 28))).toEqual(true); }); test('decrypt: xsalsa20_poly1305_suffix', () => { @@ -191,7 +195,7 @@ describe('VoiceReceiver', () => { // Assert expect(nonce.equals(range(41, 64))).toEqual(true); - expect(decrypted.equals(range(13, 40))).toEqual(true); + expect(decrypted!.equals(range(13, 40))).toEqual(true); }); test('decrypt: xsalsa20_poly1305', () => { @@ -204,7 +208,7 @@ describe('VoiceReceiver', () => { // Assert expect(nonce.equals(range(1, 12))).toEqual(true); - expect(decrypted.equals(range(13, 64))).toEqual(true); + expect(decrypted!.equals(range(13, 64))).toEqual(true); }); }); }); diff --git a/packages/voice/__tests__/VoiceUDPSocket.test.ts b/packages/voice/__tests__/VoiceUDPSocket.test.ts index 6178e215b0c3..28c79dca369e 100644 --- a/packages/voice/__tests__/VoiceUDPSocket.test.ts +++ b/packages/voice/__tests__/VoiceUDPSocket.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { Buffer } from 'node:buffer'; import { createSocket as _createSocket } from 'node:dgram'; import EventEmitter, { once } from 'node:events'; import { VoiceUDPSocket } from '../src/networking/VoiceUDPSocket'; @@ -16,6 +17,7 @@ beforeEach(() => { class FakeSocket extends EventEmitter { public send(buffer: Buffer, port: number, address: string) {} + public close() { this.emit('close'); } @@ -29,7 +31,7 @@ const VALID_RESPONSE = Buffer.from([ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd3, 0x84, ]); -function wait() { +async function wait() { return new Promise((resolve) => { setImmediate(resolve); jest.advanceTimersToNextTimer(); @@ -52,13 +54,13 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { fake.emit('message', VALID_RESPONSE); }); createSocket.mockImplementation((type) => fake as any); - socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 }); + socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 }); expect(createSocket).toHaveBeenCalledWith('udp4'); expect(fake.listenerCount('message')).toEqual(1); - await expect(socket.performIPDiscovery(1234)).resolves.toEqual({ + await expect(socket.performIPDiscovery(1_234)).resolves.toEqual({ ip: '91.90.123.93', - port: 54148, + port: 54_148, }); // Ensure clean up occurs expect(fake.listenerCount('message')).toEqual(1); @@ -77,13 +79,13 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { fake.emit('message', VALID_RESPONSE); }); createSocket.mockImplementation(() => fake as any); - socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 }); + socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 }); expect(createSocket).toHaveBeenCalledWith('udp4'); expect(fake.listenerCount('message')).toEqual(1); - await expect(socket.performIPDiscovery(1234)).resolves.toEqual({ + await expect(socket.performIPDiscovery(1_234)).resolves.toEqual({ ip: '91.90.123.93', - port: 54148, + port: 54_148, }); // Ensure clean up occurs expect(fake.listenerCount('message')).toEqual(1); @@ -96,10 +98,10 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { fake.close(); }); createSocket.mockImplementation(() => fake as any); - socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 }); + socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 }); expect(createSocket).toHaveBeenCalledWith('udp4'); - await expect(socket.performIPDiscovery(1234)).rejects.toThrowError(); + await expect(socket.performIPDiscovery(1_234)).rejects.toThrowError(); }); test('Stays alive when messages are echoed back', async () => { @@ -109,13 +111,12 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { fake.emit('message', buffer); }); createSocket.mockImplementation(() => fake as any); - socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 }); + socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 }); let closed = false; - // @ts-expect-error socket.on('close', () => (closed = true)); - for (let i = 0; i < 30; i++) { + for (let index = 0; index < 30; index++) { jest.advanceTimersToNextTimer(); await wait(); } @@ -127,13 +128,12 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { const fake = new FakeSocket(); fake.send = jest.fn(); createSocket.mockImplementation(() => fake as any); - socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 }); + socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 }); let closed = false; - // @ts-expect-error socket.on('close', () => (closed = true)); - for (let i = 0; i < 15; i++) { + for (let index = 0; index < 15; index++) { jest.advanceTimersToNextTimer(); await wait(); } @@ -146,25 +146,27 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { const fakeSend = jest.fn(); fake.send = fakeSend; createSocket.mockImplementation(() => fake as any); - socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 }); + socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 }); let closed = false; - // @ts-expect-error + socket.on('close', () => (closed = true)); - for (let i = 0; i < 10; i++) { + for (let index = 0; index < 10; index++) { jest.advanceTimersToNextTimer(); await wait(); } + fakeSend.mockImplementation(async (buffer: Buffer) => { await wait(); fake.emit('message', buffer); }); expect(closed).toEqual(false); - for (let i = 0; i < 30; i++) { + for (let index = 0; index < 30; index++) { jest.advanceTimersToNextTimer(); await wait(); } + expect(closed).toEqual(false); }); }); diff --git a/packages/voice/__tests__/VoiceWebSocket.test.ts b/packages/voice/__tests__/VoiceWebSocket.test.ts index 8ff4287dd90b..027ebe9c7711 100644 --- a/packages/voice/__tests__/VoiceWebSocket.test.ts +++ b/packages/voice/__tests__/VoiceWebSocket.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import EventEmitter, { once } from 'node:events'; +import { type EventEmitter, once } from 'node:events'; import { VoiceOpcodes } from 'discord-api-types/voice/v4'; import WS from 'jest-websocket-mock'; import { VoiceWebSocket } from '../src/networking/VoiceWebSocket'; @@ -9,13 +9,13 @@ beforeEach(() => { WS.clean(); }); -function onceIgnoreError(target: T, event: string) { +async function onceIgnoreError(target: T, event: string) { return new Promise((resolve) => { target.on(event, resolve); }); } -function onceOrThrow(target: T, event: string, after: number) { +async function onceOrThrow(target: T, event: string, after: number) { return new Promise((resolve, reject) => { target.on(event, resolve); setTimeout(() => reject(new Error('Time up')), after); @@ -46,7 +46,7 @@ describe.skip('VoiceWebSocket: packet parsing', () => { server.send('asdf'); await expect(rcv).rejects.toThrowError(); - const dummy = { op: 1234 }; + const dummy = { op: 1_234 }; rcv = once(ws, 'packet'); server.send(JSON.stringify(dummy)); await expect(rcv).resolves.toEqual([dummy]); @@ -94,17 +94,19 @@ describe.skip('VoiceWebSocket: heartbeating', () => { await server.connected; const rcv = onceOrThrow(ws, 'close', 750); ws.setHeartbeatInterval(50); - for (let i = 0; i < 10; i++) { + for (let index = 0; index < 10; index++) { const packet: any = await server.nextMessage; expect(packet).toMatchObject({ op: VoiceOpcodes.Heartbeat, }); server.send({ op: VoiceOpcodes.HeartbeatAck, + // eslint-disable-next-line id-length d: packet.d, }); expect(ws.ping).toBeGreaterThanOrEqual(0); } + ws.setHeartbeatInterval(-1); await expect(rcv).rejects.toThrowError(); }); diff --git a/packages/voice/__tests__/demuxProbe.test.ts b/packages/voice/__tests__/demuxProbe.test.ts index 3d2c8e7b41cc..a6fd78865761 100644 --- a/packages/voice/__tests__/demuxProbe.test.ts +++ b/packages/voice/__tests__/demuxProbe.test.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { Buffer } from 'node:buffer'; import EventEmitter, { once } from 'node:events'; +import process from 'node:process'; import { Readable } from 'node:stream'; import { opus as _opus } from 'prism-media'; -import { StreamType } from '../src/audio'; +import { StreamType } from '../src/audio/index'; import { demuxProbe } from '../src/util/demuxProbe'; jest.mock('prism-media'); @@ -12,19 +14,21 @@ jest.mock('prism-media'); const WebmDemuxer = _opus.WebmDemuxer as unknown as jest.Mock<_opus.WebmDemuxer>; const OggDemuxer = _opus.OggDemuxer as unknown as jest.Mock<_opus.OggDemuxer>; -function nextTick() { +async function nextTick() { + // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => process.nextTick(resolve)); } -async function* gen(n: number) { - for (let i = 0; i < n; i++) { - yield Buffer.from([i]); +async function* gen(num: number) { + for (let index = 0; index < num; index++) { + yield Buffer.from([index]); await nextTick(); } } -function range(n: number) { - return Buffer.from(Array.from(Array(n).keys())); +function range(num: number) { + // eslint-disable-next-line unicorn/no-new-array + return Buffer.from(Array.from(new Array(num).keys())); } const validHead = Buffer.from([ @@ -41,6 +45,7 @@ async function collectStream(stream: Readable): Promise { for await (const data of stream) { output = Buffer.concat([output, data]); } + return output; } @@ -107,10 +112,10 @@ describe('demuxProbe', () => { }); test('Gives up on larger streams', async () => { - const stream = Readable.from(gen(8192), { objectMode: false }); + const stream = Readable.from(gen(8_192), { objectMode: false }); const probe = await demuxProbe(stream); expect(probe.type).toEqual(StreamType.Arbitrary); - await expect(collectStream(probe.stream)).resolves.toEqual(range(8192)); + await expect(collectStream(probe.stream)).resolves.toEqual(range(8_192)); }); test('Propagates errors', async () => { diff --git a/packages/voice/__tests__/entersState.test.ts b/packages/voice/__tests__/entersState.test.ts index 3d4ab8f4b21f..e2f10d888a98 100644 --- a/packages/voice/__tests__/entersState.test.ts +++ b/packages/voice/__tests__/entersState.test.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import EventEmitter from 'node:events'; -import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection'; +import process from 'node:process'; +import { VoiceConnectionStatus, type VoiceConnection } from '../src/VoiceConnection'; import { entersState } from '../src/util/entersState'; function createFakeVoiceConnection(status = VoiceConnectionStatus.Signalling) { @@ -20,13 +21,13 @@ describe('entersState', () => { const vc = createFakeVoiceConnection(); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any)); - const result = await entersState(vc, VoiceConnectionStatus.Ready, 1000); + const result = await entersState(vc, VoiceConnectionStatus.Ready, 1_000); expect(result).toEqual(vc); }); test('Rejects once the timeout is exceeded', async () => { const vc = createFakeVoiceConnection(); - const promise = entersState(vc, VoiceConnectionStatus.Ready, 1000); + const promise = entersState(vc, VoiceConnectionStatus.Ready, 1_000); jest.runAllTimers(); await expect(promise).rejects.toThrowError(); }); @@ -51,6 +52,6 @@ describe('entersState', () => { test('Resolves immediately when target already in desired state', async () => { const vc = createFakeVoiceConnection(); - await expect(entersState(vc, VoiceConnectionStatus.Signalling, 1000)).resolves.toEqual(vc); + await expect(entersState(vc, VoiceConnectionStatus.Signalling, 1_000)).resolves.toEqual(vc); }); }); diff --git a/packages/voice/package.json b/packages/voice/package.json index 2d2d29afd045..03eb654713de 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -67,15 +67,9 @@ "@microsoft/api-extractor": "^7.29.5", "@types/jest": "^28.1.8", "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "downlevel-dts": "^0.10.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-tsdoc": "^0.2.16", + "eslint-config-neon": "^0.1.23", "jest": "^29.0.1", "jest-websocket-mock": "^2.4.0", "mock-socket": "^9.1.5", diff --git a/packages/voice/src/DataStore.ts b/packages/voice/src/DataStore.ts index 0c86c55d61b1..6e987aef5c3e 100644 --- a/packages/voice/src/DataStore.ts +++ b/packages/voice/src/DataStore.ts @@ -3,11 +3,11 @@ import type { VoiceConnection } from './VoiceConnection'; import type { AudioPlayer } from './audio'; export interface JoinConfig { - guildId: string; channelId: string | null; + group: string; + guildId: string; selfDeaf: boolean; selfMute: boolean; - group: string; } /** @@ -19,6 +19,7 @@ export interface JoinConfig { export function createJoinVoiceChannelPayload(config: JoinConfig) { return { op: GatewayOpcodes.VoiceStateUpdate, + // eslint-disable-next-line id-length d: { guild_id: config.guildId, channel_id: config.channelId, @@ -54,7 +55,6 @@ export function getGroups() { * Retrieves all the voice connections under the 'default' group. * * @param group - The group to look up - * * @returns The map of voice connections */ export function getVoiceConnections(group?: 'default'): Map; @@ -63,7 +63,6 @@ export function getVoiceConnections(group?: 'default'): Map | undefined; @@ -72,7 +71,6 @@ export function getVoiceConnections(group: string): Map * Retrieves all the voice connections under the given group name. Defaults to the 'default' group. * * @param group - The group to look up - * * @returns The map of voice connections */ export function getVoiceConnections(group = 'default') { @@ -84,7 +82,6 @@ export function getVoiceConnections(group = 'default') { * * @param guildId - The guild id of the voice connection * @param group - the group that the voice connection was registered with - * * @returns The voice connection, if it exists */ export function getVoiceConnection(guildId: string, group = 'default') { @@ -122,8 +119,10 @@ function audioCycleStep() { nextTime += FRAME_LENGTH; const available = audioPlayers.filter((player) => player.checkPlayable()); - // eslint-disable-next-line @typescript-eslint/dot-notation - available.forEach((player) => player['_stepDispatch']()); + for (const player of available) { + // eslint-disable-next-line @typescript-eslint/dot-notation + player['_stepDispatch'](); + } // eslint-disable-next-line @typescript-eslint/no-use-before-define prepareNextAudioFrame(available); @@ -140,6 +139,7 @@ function prepareNextAudioFrame(players: AudioPlayer[]) { if (nextTime !== -1) { audioCycleInterval = setTimeout(() => audioCycleStep(), nextTime - Date.now()); } + return; } @@ -154,7 +154,6 @@ function prepareNextAudioFrame(players: AudioPlayer[]) { * Checks whether or not the given audio player is being driven by the data store clock. * * @param target - The target to test for - * * @returns `true` if it is being tracked, `false` otherwise */ export function hasAudioPlayer(target: AudioPlayer) { @@ -173,6 +172,7 @@ export function addAudioPlayer(player: AudioPlayer) { nextTime = Date.now(); setImmediate(() => audioCycleStep()); } + return player; } diff --git a/packages/voice/src/VoiceConnection.ts b/packages/voice/src/VoiceConnection.ts index eb76482c98fa..d77fe2af8f65 100644 --- a/packages/voice/src/VoiceConnection.ts +++ b/packages/voice/src/VoiceConnection.ts @@ -1,40 +1,39 @@ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable jsdoc/check-param-names */ /* eslint-disable @typescript-eslint/method-signature-style */ +import type { Buffer } from 'node:buffer'; import { EventEmitter } from 'node:events'; import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10'; -import type { CreateVoiceConnectionOptions } from '.'; +import type { JoinConfig } from './DataStore'; import { getVoiceConnection, createJoinVoiceChannelPayload, trackVoiceConnection, - JoinConfig, untrackVoiceConnection, } from './DataStore'; import type { AudioPlayer } from './audio/AudioPlayer'; import type { PlayerSubscription } from './audio/PlayerSubscription'; import type { VoiceWebSocket, VoiceUDPSocket } from './networking'; -import { Networking, NetworkingState, NetworkingStatusCode } from './networking/Networking'; -import { VoiceReceiver } from './receive'; +import { Networking, NetworkingStatusCode, type NetworkingState } from './networking/Networking'; +import { VoiceReceiver } from './receive/index'; import type { DiscordGatewayAdapterImplementerMethods } from './util/adapter'; import { noop } from './util/util'; +import type { CreateVoiceConnectionOptions } from './index'; /** * The various status codes a voice connection can hold at any one time. */ export enum VoiceConnectionStatus { - /** - * Sending a packet to the main Discord gateway to indicate we want to change our voice state. - */ - Signalling = 'signalling', - /** * The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection. */ Connecting = 'connecting', /** - * A voice connection has been established, and is ready to be used. + * The voice connection has been destroyed and untracked, it cannot be reused. */ - Ready = 'ready', + Destroyed = 'destroyed', /** * The voice connection has either been severed or not established. @@ -42,9 +41,14 @@ export enum VoiceConnectionStatus { Disconnected = 'disconnected', /** - * The voice connection has been destroyed and untracked, it cannot be reused. + * A voice connection has been established, and is ready to be used. */ - Destroyed = 'destroyed', + Ready = 'ready', + + /** + * Sending a packet to the main Discord gateway to indicate we want to change our voice state. + */ + Signalling = 'signalling', } /** @@ -52,9 +56,9 @@ export enum VoiceConnectionStatus { * VOICE_STATE_UPDATE packet from Discord, provided by the adapter. */ export interface VoiceConnectionSignallingState { + adapter: DiscordGatewayAdapterImplementerMethods; status: VoiceConnectionStatus.Signalling; subscription?: PlayerSubscription | undefined; - adapter: DiscordGatewayAdapterImplementerMethods; } /** @@ -87,9 +91,9 @@ export enum VoiceConnectionDisconnectReason { * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect. */ export interface VoiceConnectionDisconnectedBaseState { + adapter: DiscordGatewayAdapterImplementerMethods; status: VoiceConnectionStatus.Disconnected; subscription?: PlayerSubscription | undefined; - adapter: DiscordGatewayAdapterImplementerMethods; } /** @@ -105,12 +109,12 @@ export interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDi * You can manually attempt to reconnect using VoiceConnection#reconnect. */ export interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState { - reason: VoiceConnectionDisconnectReason.WebSocketClose; - /** * The close code of the WebSocket connection to the Discord voice server. */ closeCode: number; + + reason: VoiceConnectionDisconnectReason.WebSocketClose; } /** @@ -126,10 +130,10 @@ export type VoiceConnectionDisconnectedState = * voice server. */ export interface VoiceConnectionConnectingState { - status: VoiceConnectionStatus.Connecting; + adapter: DiscordGatewayAdapterImplementerMethods; networking: Networking; + status: VoiceConnectionStatus.Connecting; subscription?: PlayerSubscription | undefined; - adapter: DiscordGatewayAdapterImplementerMethods; } /** @@ -137,10 +141,10 @@ export interface VoiceConnectionConnectingState { * voice server. */ export interface VoiceConnectionReadyState { - status: VoiceConnectionStatus.Ready; + adapter: DiscordGatewayAdapterImplementerMethods; networking: Networking; + status: VoiceConnectionStatus.Ready; subscription?: PlayerSubscription | undefined; - adapter: DiscordGatewayAdapterImplementerMethods; } /** @@ -156,30 +160,34 @@ export interface VoiceConnectionDestroyedState { * The various states that a voice connection can be in. */ export type VoiceConnectionState = - | VoiceConnectionSignallingState - | VoiceConnectionDisconnectedState | VoiceConnectionConnectingState + | VoiceConnectionDestroyedState + | VoiceConnectionDisconnectedState | VoiceConnectionReadyState - | VoiceConnectionDestroyedState; + | VoiceConnectionSignallingState; export interface VoiceConnection extends EventEmitter { /** * Emitted when there is an error emitted from the voice connection + * * @eventProperty */ on(event: 'error', listener: (error: Error) => void): this; /** * Emitted debugging information about the voice connection + * * @eventProperty */ on(event: 'debug', listener: (message: string) => void): this; /** * Emitted when the state of the voice connection changes + * * @eventProperty */ on(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this; /** * Emitted when the state of the voice connection changes to a specific status + * * @eventProperty */ on( @@ -228,7 +236,7 @@ export class VoiceConnection extends EventEmitter { /** * The debug logger function, if debugging is enabled. */ - private readonly debug: null | ((message: string) => void); + private readonly debug: ((message: string) => void) | null; /** * Creates a new voice connection. @@ -292,6 +300,7 @@ export class VoiceConnection extends EventEmitter { oldNetworking.off('stateChange', this.onNetworkingStateChange); oldNetworking.destroy(); } + if (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state); } @@ -362,6 +371,7 @@ export class VoiceConnection extends EventEmitter { /** * Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound * to the new instances. + * * @param newState - The new networking state * @param oldState - The old networking state, if there is one */ @@ -432,13 +442,12 @@ export class VoiceConnection extends EventEmitter { * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state. - * * @param code - The close code */ private onNetworkingClose(code: number) { if (this.state.status === VoiceConnectionStatus.Destroyed) return; // If networking closes, try to connect to the voice channel again. - if (code === 4014) { + if (code === 4_014) { // Disconnected - networking is already destroyed here this.state = { ...this.state, @@ -548,12 +557,15 @@ export class VoiceConnection extends EventEmitter { if (this.state.status === VoiceConnectionStatus.Destroyed) { throw new Error('Cannot destroy VoiceConnection - it has already been destroyed'); } + if (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) { untrackVoiceConnection(this); } + if (adapterAvailable) { this.state.adapter.sendPayload(createJoinVoiceChannelPayload({ ...this.joinConfig, channelId: null })); } + this.state = { status: VoiceConnectionStatus.Destroyed, }; @@ -571,6 +583,7 @@ export class VoiceConnection extends EventEmitter { ) { return false; } + this.joinConfig.channelId = null; if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { this.state = { @@ -581,6 +594,7 @@ export class VoiceConnection extends EventEmitter { }; return false; } + this.state = { adapter: this.state.adapter, reason: VoiceConnectionDisconnectReason.Manual, @@ -599,7 +613,7 @@ export class VoiceConnection extends EventEmitter { * * A state transition from Disconnected to Signalling will be observed when this is called. */ - public rejoin(joinConfig?: Omit) { + public rejoin(joinConfig?: Omit) { if (this.state.status === VoiceConnectionStatus.Destroyed) { return false; } @@ -615,6 +629,7 @@ export class VoiceConnection extends EventEmitter { status: VoiceConnectionStatus.Signalling, }; } + return true; } @@ -635,6 +650,7 @@ export class VoiceConnection extends EventEmitter { */ public setSpeaking(enabled: boolean) { if (this.state.status !== VoiceConnectionStatus.Ready) return false; + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression return this.state.networking.setSpeaking(enabled); } @@ -642,7 +658,6 @@ export class VoiceConnection extends EventEmitter { * Subscribes to an audio player, allowing the player to play audio on this voice connection. * * @param player - The audio player to subscribe to - * * @returns The created subscription */ public subscribe(player: AudioPlayer) { @@ -677,6 +692,7 @@ export class VoiceConnection extends EventEmitter { udp: this.state.networking.state.udp.ping, }; } + return { ws: undefined, udp: undefined, @@ -721,19 +737,22 @@ export function createVoiceConnection(joinConfig: JoinConfig, options: CreateVoi reason: VoiceConnectionDisconnectReason.AdapterUnavailable, }; } + return existing; } const voiceConnection = new VoiceConnection(joinConfig, options); trackVoiceConnection(voiceConnection); - if (voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) { - if (!voiceConnection.state.adapter.sendPayload(payload)) { - voiceConnection.state = { - ...voiceConnection.state, - status: VoiceConnectionStatus.Disconnected, - reason: VoiceConnectionDisconnectReason.AdapterUnavailable, - }; - } + if ( + voiceConnection.state.status !== VoiceConnectionStatus.Destroyed && + !voiceConnection.state.adapter.sendPayload(payload) + ) { + voiceConnection.state = { + ...voiceConnection.state, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.AdapterUnavailable, + }; } + return voiceConnection; } diff --git a/packages/voice/src/audio/AudioPlayer.ts b/packages/voice/src/audio/AudioPlayer.ts index 25d73361cd85..a5a25acd8325 100644 --- a/packages/voice/src/audio/AudioPlayer.ts +++ b/packages/voice/src/audio/AudioPlayer.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/method-signature-style */ +import { Buffer } from 'node:buffer'; import EventEmitter from 'node:events'; +import { addAudioPlayer, deleteAudioPlayer } from '../DataStore'; +import { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection'; +import { noop } from '../util/util'; import { AudioPlayerError } from './AudioPlayerError'; import type { AudioResource } from './AudioResource'; import { PlayerSubscription } from './PlayerSubscription'; -import { addAudioPlayer, deleteAudioPlayer } from '../DataStore'; -import { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection'; -import { noop } from '../util/util'; // The Opus "silent" frame export const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]); @@ -33,15 +34,20 @@ export enum NoSubscriberBehavior { export enum AudioPlayerStatus { /** - * When there is currently no resource for the player to be playing. + * When the player has paused itself. Only possible with the "pause" no subscriber behavior. */ - Idle = 'idle', + AutoPaused = 'autopaused', /** * When the player is waiting for an audio resource to become readable before transitioning to Playing. */ Buffering = 'buffering', + /** + * When there is currently no resource for the player to be playing. + */ + Idle = 'idle', + /** * When the player has been manually paused. */ @@ -51,22 +57,17 @@ export enum AudioPlayerStatus { * When the player is actively playing an audio resource. */ Playing = 'playing', - - /** - * When the player has paused itself. Only possible with the "pause" no subscriber behavior. - */ - AutoPaused = 'autopaused', } /** * Options that can be passed when creating an audio player, used to specify its behavior. */ export interface CreateAudioPlayerOptions { - debug?: boolean; behaviors?: { - noSubscriber?: NoSubscriberBehavior; maxMissedFrames?: number; + noSubscriber?: NoSubscriberBehavior; }; + debug?: boolean; } /** @@ -82,14 +83,14 @@ export interface AudioPlayerIdleState { * it will re-enter the Idle state. */ export interface AudioPlayerBufferingState { - status: AudioPlayerStatus.Buffering; + onFailureCallback: () => void; + onReadableCallback: () => void; + onStreamError: (error: Error) => void; /** * The resource that the AudioPlayer is waiting for */ resource: AudioResource; - onReadableCallback: () => void; - onFailureCallback: () => void; - onStreamError: (error: Error) => void; + status: AudioPlayerStatus.Buffering; } /** @@ -97,11 +98,11 @@ export interface AudioPlayerBufferingState { * it will enter the Idle state. */ export interface AudioPlayerPlayingState { - status: AudioPlayerStatus.Playing; /** * The number of consecutive times that the audio resource has been unable to provide an Opus frame. */ missedFrames: number; + onStreamError: (error: Error) => void; /** * The playback duration in milliseconds of the current audio resource. This includes filler silence packets @@ -114,7 +115,7 @@ export interface AudioPlayerPlayingState { */ resource: AudioResource; - onStreamError: (error: Error) => void; + status: AudioPlayerStatus.Playing; } /** @@ -122,12 +123,7 @@ export interface AudioPlayerPlayingState { * automatically by the AudioPlayer itself if there are no available subscribers. */ export interface AudioPlayerPausedState { - status: AudioPlayerStatus.Paused | AudioPlayerStatus.AutoPaused; - /** - * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing. - */ - silencePacketsRemaining: number; - + onStreamError: (error: Error) => void; /** * The playback duration in milliseconds of the current audio resource. This includes filler silence packets * that have been played when the resource was buffering. @@ -139,41 +135,51 @@ export interface AudioPlayerPausedState { */ resource: AudioResource; - onStreamError: (error: Error) => void; + /** + * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing. + */ + silencePacketsRemaining: number; + + status: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused; } /** * The various states that the player can be in. */ export type AudioPlayerState = - | AudioPlayerIdleState | AudioPlayerBufferingState - | AudioPlayerPlayingState - | AudioPlayerPausedState; + | AudioPlayerIdleState + | AudioPlayerPausedState + | AudioPlayerPlayingState; export interface AudioPlayer extends EventEmitter { /** * Emitted when there is an error emitted from the audio resource played by the audio player + * * @eventProperty */ on(event: 'error', listener: (error: AudioPlayerError) => void): this; /** * Emitted debugging information about the audio player + * * @eventProperty */ on(event: 'debug', listener: (message: string) => void): this; /** * Emitted when the state of the audio player changes + * * @eventProperty */ on(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this; /** * Emitted when the audio player is subscribed to a voice connection + * * @eventProperty */ on(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this; /** * Emitted when the status of state changes to a specific status + * * @eventProperty */ on( @@ -221,14 +227,14 @@ export class AudioPlayer extends EventEmitter { * The behavior that the player should follow when it enters certain situations. */ private readonly behaviors: { - noSubscriber: NoSubscriberBehavior; maxMissedFrames: number; + noSubscriber: NoSubscriberBehavior; }; /** * The debug logger function, if debugging is enabled. */ - private readonly debug: null | ((message: string) => void); + private readonly debug: ((message: string) => void) | null; /** * Creates a new AudioPlayer. @@ -259,9 +265,7 @@ export class AudioPlayer extends EventEmitter { * * @remarks * This method should not be directly called. Instead, use VoiceConnection#subscribe. - * * @param connection - The connection to subscribe - * * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription */ // @ts-ignore @@ -273,6 +277,7 @@ export class AudioPlayer extends EventEmitter { setImmediate(() => this.emit('subscribe', subscription)); return subscription; } + return existingSubscription; } @@ -281,9 +286,7 @@ export class AudioPlayer extends EventEmitter { * * @remarks * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe. - * * @param subscription - The subscription to remove - * * @returns Whether or not the subscription existed on the player and was removed */ // @ts-ignore @@ -295,6 +298,7 @@ export class AudioPlayer extends EventEmitter { subscription.connection.setSpeaking(false); this.emit('unsubscribe', subscription); } + return exists; } @@ -355,6 +359,7 @@ export class AudioPlayer extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.emit(newState.status, oldState, this._state as any); } + this.debug?.(`state change:\nfrom ${stringifyState(oldState)}\nto ${stringifyState(newState)}`); } @@ -368,9 +373,7 @@ export class AudioPlayer extends EventEmitter { * * If the player was previously playing a resource and this method is called, the player will not transition to the * Idle state during the swap over. - * * @param resource - The resource to play - * * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player */ public play(resource: AudioResource) { @@ -382,8 +385,10 @@ export class AudioPlayer extends EventEmitter { if (resource.audioPlayer === this) { return; } + throw new Error('Resource is already being played by another audio player.'); } + resource.audioPlayer = this; // Attach error listeners to the stream that will propagate the error and then return to the Idle @@ -451,7 +456,6 @@ export class AudioPlayer extends EventEmitter { * Pauses playback of the current resource, if any. * * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches - * * @returns `true` if the player was successfully paused, otherwise `false` */ public pause(interpolateSilence = true) { @@ -484,7 +488,6 @@ export class AudioPlayer extends EventEmitter { * or remain in its current state until the silence padding frames of the resource have been played. * * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames - * * @returns `true` if the player will come to a stop, otherwise `false` */ public stop(force = false) { @@ -496,6 +499,7 @@ export class AudioPlayer extends EventEmitter { } else if (this.state.resource.silenceRemaining === -1) { this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames; } + return true; } @@ -515,6 +519,7 @@ export class AudioPlayer extends EventEmitter { }; return false; } + return true; } @@ -530,7 +535,9 @@ export class AudioPlayer extends EventEmitter { if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return; // Dispatch any audio packets that were prepared in the previous cycle - this.playable.forEach((connection) => connection.dispatchAudio()); + for (const connection of this.playable) { + connection.dispatchAudio(); + } } /** @@ -568,6 +575,7 @@ export class AudioPlayer extends EventEmitter { this._signalStopSpeaking(); } } + return; } @@ -612,7 +620,9 @@ export class AudioPlayer extends EventEmitter { * they are no longer speaking. Called once playback of a resource ends. */ private _signalStopSpeaking() { - return this.subscribers.forEach(({ connection }) => connection.setSpeaking(false)); + for (const { connection } of this.subscribers) { + connection.setSpeaking(false); + } } /** @@ -625,10 +635,12 @@ export class AudioPlayer extends EventEmitter { private _preparePacket( packet: Buffer, receivers: VoiceConnection[], - state: AudioPlayerPlayingState | AudioPlayerPausedState, + state: AudioPlayerPausedState | AudioPlayerPlayingState, ) { state.playbackDuration += 20; - receivers.forEach((connection) => connection.prepareAudioPacket(packet)); + for (const connection of receivers) { + connection.prepareAudioPacket(packet); + } } } diff --git a/packages/voice/src/audio/AudioPlayerError.ts b/packages/voice/src/audio/AudioPlayerError.ts index 4b16e802c3cf..eb7344580d59 100644 --- a/packages/voice/src/audio/AudioPlayerError.ts +++ b/packages/voice/src/audio/AudioPlayerError.ts @@ -9,6 +9,7 @@ export class AudioPlayerError extends Error { * The resource associated with the audio player at the time the error was thrown. */ public readonly resource: AudioResource; + public constructor(error: Error, resource: AudioResource) { super(error.message); this.resource = resource; diff --git a/packages/voice/src/audio/AudioResource.ts b/packages/voice/src/audio/AudioResource.ts index 61b1d9cc8a15..0deaccf32bc1 100644 --- a/packages/voice/src/audio/AudioResource.ts +++ b/packages/voice/src/audio/AudioResource.ts @@ -1,8 +1,9 @@ -import { pipeline, Readable } from 'node:stream'; +import type { Buffer } from 'node:buffer'; +import { pipeline, type Readable } from 'node:stream'; import prism from 'prism-media'; -import { AudioPlayer, SILENCE_FRAME } from './AudioPlayer'; -import { Edge, findPipeline, StreamType, TransformerType } from './TransformerGraph'; import { noop } from '../util/util'; +import { SILENCE_FRAME, type AudioPlayer } from './AudioPlayer'; +import { findPipeline, StreamType, TransformerType, type Edge } from './TransformerGraph'; /** * Options that are set when creating a new audio resource. @@ -10,6 +11,12 @@ import { noop } from '../util/util'; * @typeParam T - the type for the metadata (if any) of the audio resource */ export interface CreateAudioResourceOptions { + /** + * Whether or not inline volume should be enabled. If enabled, you will be able to change the volume + * of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`. + */ + inlineVolume?: boolean; + /** * The type of the input stream. Defaults to `StreamType.Arbitrary`. */ @@ -22,12 +29,6 @@ export interface CreateAudioResourceOptions { */ metadata?: T; - /** - * Whether or not inline volume should be enabled. If enabled, you will be able to change the volume - * of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`. - */ - inlineVolume?: boolean; - /** * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. * Defaults to 5. @@ -123,6 +124,7 @@ export class AudioResource { if (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames; return this.silenceRemaining !== 0; } + return real; } @@ -141,7 +143,6 @@ export class AudioResource { * It is advisable to check that the playStream is readable before calling this method. While no runtime * errors will be thrown, you should check that the resource is still available before attempting to * read from it. - * * @internal */ public read(): Buffer | null { @@ -151,10 +152,12 @@ export class AudioResource { this.silenceRemaining--; return SILENCE_FRAME; } + const packet = this.playStream.read() as Buffer | null; if (packet) { this.playbackDuration += 20; } + return packet; } } @@ -174,8 +177,8 @@ export const NO_CONSTRAINT = () => true; * @param stream - The stream to infer the type of */ export function inferStreamType(stream: Readable): { - streamType: StreamType; hasVolume: boolean; + streamType: StreamType; } { if (stream instanceof prism.opus.Encoder) { return { streamType: StreamType.Opus, hasVolume: false }; @@ -188,6 +191,7 @@ export function inferStreamType(stream: Readable): { } else if (stream instanceof prism.opus.WebmDemuxer) { return { streamType: StreamType.Opus, hasVolume: false }; } + return { streamType: StreamType.Arbitrary, hasVolume: false }; } @@ -200,14 +204,12 @@ export function inferStreamType(stream: Readable): { * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, * Opus transcoders, and Ogg/WebM demuxers. - * * @param input - The resource to play * @param options - Configurable options for creating the resource - * * @typeParam T - the type for the metadata (if any) of the audio resource */ export function createAudioResource( - input: string | Readable, + input: Readable | string, options: CreateAudioResourceOptions & Pick< T extends null | undefined ? CreateAudioResourceOptions : Required>, @@ -224,14 +226,12 @@ export function createAudioResource( * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, * Opus transcoders, and Ogg/WebM demuxers. - * * @param input - The resource to play * @param options - Configurable options for creating the resource - * * @typeParam T - the type for the metadata (if any) of the audio resource */ export function createAudioResource( - input: string | Readable, + input: Readable | string, options?: Omit, 'metadata'>, ): AudioResource; @@ -244,14 +244,12 @@ export function createAudioResource( * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, * Opus transcoders, and Ogg/WebM demuxers. - * * @param input - The resource to play * @param options - Configurable options for creating the resource - * * @typeParam T - the type for the metadata (if any) of the audio resource */ export function createAudioResource( - input: string | Readable, + input: Readable | string, options: CreateAudioResourceOptions = {}, ): AudioResource { let inputType = options.inputType; @@ -273,6 +271,7 @@ export function createAudioResource( // No adjustments required return new AudioResource([], [input], (options.metadata ?? null) as T, options.silencePaddingFrames ?? 5); } + const streams = transformerPipeline.map((edge) => edge.transformer(input)); if (typeof input !== 'string') streams.unshift(input); diff --git a/packages/voice/src/audio/PlayerSubscription.ts b/packages/voice/src/audio/PlayerSubscription.ts index 9e56a0480e5b..e3d6fddba102 100644 --- a/packages/voice/src/audio/PlayerSubscription.ts +++ b/packages/voice/src/audio/PlayerSubscription.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/dot-notation */ -import type { AudioPlayer } from './AudioPlayer'; import type { VoiceConnection } from '../VoiceConnection'; +import type { AudioPlayer } from './AudioPlayer'; /** * Represents a subscription of a voice connection to an audio player, allowing diff --git a/packages/voice/src/audio/TransformerGraph.ts b/packages/voice/src/audio/TransformerGraph.ts index ada56387a293..fbb8d3252dcb 100644 --- a/packages/voice/src/audio/TransformerGraph.ts +++ b/packages/voice/src/audio/TransformerGraph.ts @@ -34,33 +34,33 @@ const FFMPEG_OPUS_ARGUMENTS = [ */ export enum StreamType { Arbitrary = 'arbitrary', - Raw = 'raw', OggOpus = 'ogg/opus', - WebmOpus = 'webm/opus', Opus = 'opus', + Raw = 'raw', + WebmOpus = 'webm/opus', } /** * The different types of transformers that can exist within the pipeline. */ export enum TransformerType { - FFmpegPCM = 'ffmpeg pcm', FFmpegOgg = 'ffmpeg ogg', - OpusEncoder = 'opus encoder', - OpusDecoder = 'opus decoder', + FFmpegPCM = 'ffmpeg pcm', + InlineVolume = 'volume transformer', OggOpusDemuxer = 'ogg/opus demuxer', + OpusDecoder = 'opus decoder', + OpusEncoder = 'opus encoder', WebmOpusDemuxer = 'webm/opus demuxer', - InlineVolume = 'volume transformer', } /** * Represents a pathway from one stream type to another using a transformer. */ export interface Edge { + cost: number; from: Node; to: Node; - cost: number; - transformer: (input: string | Readable) => Readable; + transformer(input: Readable | string): Readable; type: TransformerType; } @@ -113,14 +113,14 @@ getNode(StreamType.Raw).addEdge({ type: TransformerType.OpusEncoder, to: getNode(StreamType.Opus), cost: 1.5, - transformer: () => new prism.opus.Encoder({ rate: 48000, channels: 2, frameSize: 960 }), + transformer: () => new prism.opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }), }); getNode(StreamType.Opus).addEdge({ type: TransformerType.OpusDecoder, to: getNode(StreamType.Raw), cost: 1.5, - transformer: () => new prism.opus.Decoder({ rate: 48000, channels: 2, frameSize: 960 }), + transformer: () => new prism.opus.Decoder({ rate: 48_000, channels: 2, frameSize: 960 }), }); getNode(StreamType.OggOpus).addEdge({ @@ -163,6 +163,7 @@ function canEnableFFmpegOptimizations(): boolean { try { return prism.FFmpeg.getInfo().output.includes('--enable-libopus'); } catch {} + return false; } @@ -188,11 +189,6 @@ if (canEnableFFmpegOptimizations()) { * Represents a step in the path from node A to node B. */ interface Step { - /** - * The next step. - */ - next?: Step; - /** * The cost of the steps after this step. */ @@ -202,6 +198,11 @@ interface Step { * The edge associated with this step. */ edge?: Edge; + + /** + * The next step. + */ + next?: Step; } /** @@ -223,10 +224,10 @@ function findPath( if (from === goal && constraints(path)) { return { cost: 0 }; } else if (depth === 0) { - return { cost: Infinity }; + return { cost: Number.POSITIVE_INFINITY }; } - let currentBest: Step | undefined = undefined; + let currentBest: Step | undefined; for (const edge of from.edges) { if (currentBest && edge.cost > currentBest.cost) continue; const next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1); @@ -235,7 +236,8 @@ function findPath( currentBest = { cost, edge, next }; } } - return currentBest ?? { cost: Infinity }; + + return currentBest ?? { cost: Number.POSITIVE_INFINITY }; } /** @@ -250,6 +252,7 @@ function constructPipeline(step: Step) { edges.push(current.edge); current = current.next; } + return edges; } diff --git a/packages/voice/src/index.ts b/packages/voice/src/index.ts index 13698c0f18c5..2ed09b69ed3b 100644 --- a/packages/voice/src/index.ts +++ b/packages/voice/src/index.ts @@ -1,7 +1,7 @@ export * from './joinVoiceChannel'; -export * from './audio'; -export * from './util'; -export * from './receive'; +export * from './audio/index'; +export * from './util/index'; +export * from './receive/index'; export { VoiceConnection, diff --git a/packages/voice/src/joinVoiceChannel.ts b/packages/voice/src/joinVoiceChannel.ts index 8ff5e8c65206..15c083c75e1d 100644 --- a/packages/voice/src/joinVoiceChannel.ts +++ b/packages/voice/src/joinVoiceChannel.ts @@ -6,13 +6,13 @@ import type { DiscordGatewayAdapterCreator } from './util/adapter'; * The options that can be given when creating a voice connection. */ export interface CreateVoiceConnectionOptions { + adapterCreator: DiscordGatewayAdapterCreator; + /** * If true, debug messages will be enabled for the voice connection and its * related components. Defaults to false. */ debug?: boolean | undefined; - - adapterCreator: DiscordGatewayAdapterCreator; } /** @@ -24,6 +24,11 @@ export interface JoinVoiceChannelOptions { */ channelId: string; + /** + * An optional group identifier for the voice connection. + */ + group?: string; + /** * The id of the guild that the voice channel belongs to. */ @@ -38,20 +43,14 @@ export interface JoinVoiceChannelOptions { * Whether to join the channel muted (defaults to true) */ selfMute?: boolean; - - /** - * An optional group identifier for the voice connection. - */ - group?: string; } /** * Creates a VoiceConnection to a Discord voice channel. * - * @param voiceChannel - the voice channel to connect to * @param options - the options for joining the voice channel */ -export function joinVoiceChannel(options: JoinVoiceChannelOptions & CreateVoiceConnectionOptions) { +export function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoiceChannelOptions) { const joinConfig: JoinConfig = { selfDeaf: true, selfMute: false, diff --git a/packages/voice/src/networking/Networking.ts b/packages/voice/src/networking/Networking.ts index 7af99ba2dd38..af53da86986f 100644 --- a/packages/voice/src/networking/Networking.ts +++ b/packages/voice/src/networking/Networking.ts @@ -1,15 +1,19 @@ +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable id-length */ +/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/method-signature-style */ +import { Buffer } from 'node:buffer'; import { EventEmitter } from 'node:events'; import { VoiceOpcodes } from 'discord-api-types/voice/v4'; import type { CloseEvent } from 'ws'; -import { VoiceUDPSocket } from './VoiceUDPSocket'; -import { VoiceWebSocket } from './VoiceWebSocket'; import * as secretbox from '../util/Secretbox'; import { noop } from '../util/util'; +import { VoiceUDPSocket } from './VoiceUDPSocket'; +import { VoiceWebSocket } from './VoiceWebSocket'; // The number of audio channels required by Discord const CHANNELS = 2; -const TIMESTAMP_INC = (48000 / 100) * CHANNELS; +const TIMESTAMP_INC = (48_000 / 100) * CHANNELS; const MAX_NONCE_SIZE = 2 ** 32 - 1; export const SUPPORTED_ENCRYPTION_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305']; @@ -35,8 +39,8 @@ export enum NetworkingStatusCode { */ export interface NetworkingOpeningWsState { code: NetworkingStatusCode.OpeningWs; - ws: VoiceWebSocket; connectionOptions: ConnectionOptions; + ws: VoiceWebSocket; } /** @@ -44,8 +48,8 @@ export interface NetworkingOpeningWsState { */ export interface NetworkingIdentifyingState { code: NetworkingStatusCode.Identifying; - ws: VoiceWebSocket; connectionOptions: ConnectionOptions; + ws: VoiceWebSocket; } /** @@ -54,10 +58,10 @@ export interface NetworkingIdentifyingState { */ export interface NetworkingUdpHandshakingState { code: NetworkingStatusCode.UdpHandshaking; - ws: VoiceWebSocket; - udp: VoiceUDPSocket; - connectionOptions: ConnectionOptions; connectionData: Pick; + connectionOptions: ConnectionOptions; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; } /** @@ -65,10 +69,10 @@ export interface NetworkingUdpHandshakingState { */ export interface NetworkingSelectingProtocolState { code: NetworkingStatusCode.SelectingProtocol; - ws: VoiceWebSocket; - udp: VoiceUDPSocket; - connectionOptions: ConnectionOptions; connectionData: Pick; + connectionOptions: ConnectionOptions; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; } /** @@ -77,11 +81,11 @@ export interface NetworkingSelectingProtocolState { */ export interface NetworkingReadyState { code: NetworkingStatusCode.Ready; - ws: VoiceWebSocket; - udp: VoiceUDPSocket; - connectionOptions: ConnectionOptions; connectionData: ConnectionData; + connectionOptions: ConnectionOptions; preparedPacket?: Buffer | undefined; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; } /** @@ -90,11 +94,11 @@ export interface NetworkingReadyState { */ export interface NetworkingResumingState { code: NetworkingStatusCode.Resuming; - ws: VoiceWebSocket; - udp: VoiceUDPSocket; - connectionOptions: ConnectionOptions; connectionData: ConnectionData; + connectionOptions: ConnectionOptions; preparedPacket?: Buffer | undefined; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; } /** @@ -109,13 +113,13 @@ export interface NetworkingClosedState { * The various states that a networking instance can be in. */ export type NetworkingState = - | NetworkingOpeningWsState + | NetworkingClosedState | NetworkingIdentifyingState - | NetworkingUdpHandshakingState - | NetworkingSelectingProtocolState + | NetworkingOpeningWsState | NetworkingReadyState | NetworkingResumingState - | NetworkingClosedState; + | NetworkingSelectingProtocolState + | NetworkingUdpHandshakingState; /** * Details required to connect to the Discord voice gateway. These details @@ -123,11 +127,11 @@ export type NetworkingState = * and VOICE_STATE_UPDATE packets. */ interface ConnectionOptions { + endpoint: string; serverId: string; - userId: string; sessionId: string; token: string; - endpoint: string; + userId: string; } /** @@ -135,15 +139,15 @@ interface ConnectionOptions { * the connection, timing information for playback of streams. */ export interface ConnectionData { - ssrc: number; encryptionMode: string; - secretKey: Uint8Array; - sequence: number; - timestamp: number; - packetsPlayed: number; nonce: number; nonceBuffer: Buffer; + packetsPlayed: number; + secretKey: Uint8Array; + sequence: number; speaking: boolean; + ssrc: number; + timestamp: number; } /** @@ -186,16 +190,17 @@ function chooseEncryptionMode(options: string[]): string { if (!option) { throw new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`); } + return option; } /** * Returns a random number that is in the range of n bits. * - * @param n - The number of bits + * @param numberOfBits - The number of bits */ -function randomNBit(n: number) { - return Math.floor(Math.random() * 2 ** n); +function randomNBit(numberOfBits: number) { + return Math.floor(Math.random() * 2 ** numberOfBits); } /** @@ -207,7 +212,7 @@ export class Networking extends EventEmitter { /** * The debug logger function, if debugging is enabled. */ - private readonly debug: null | ((message: string) => void); + private readonly debug: ((message: string) => void) | null; /** * Creates a new Networking instance. @@ -287,7 +292,6 @@ export class Networking extends EventEmitter { * Creates a new WebSocket to a Discord Voice gateway. * * @param endpoint - The endpoint to connect to - * @param debug - Whether to enable debug logging */ private createWebSocket(endpoint: string) { const ws = new VoiceWebSocket(`wss://${endpoint}?v=4`, Boolean(this.debug)); @@ -351,7 +355,7 @@ export class Networking extends EventEmitter { * @param code - The close code */ private onWsClose({ code }: CloseEvent) { - const canResume = code === 4015 || code < 4000; + const canResume = code === 4_015 || code < 4_000; if (canResume && this.state.code === NetworkingStatusCode.Ready) { this.state = { ...this.state, @@ -400,6 +404,7 @@ export class Networking extends EventEmitter { udp // eslint-disable-next-line @typescript-eslint/no-unsafe-argument .performIPDiscovery(ssrc) + // eslint-disable-next-line promise/prefer-await-to-then .then((localConfig) => { if (this.state.code !== NetworkingStatusCode.UdpHandshaking) return; this.state.ws.sendPacket({ @@ -419,6 +424,7 @@ export class Networking extends EventEmitter { code: NetworkingStatusCode.SelectingProtocol, }; }) + // eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-await-to-callbacks .catch((error: Error) => this.emit('error', error)); this.state = { @@ -489,15 +495,14 @@ export class Networking extends EventEmitter { * @remarks * Calling this method while there is already a prepared audio packet that has not yet been dispatched * will overwrite the existing audio packet. This should be avoided. - * * @param opusPacket - The Opus packet to encrypt - * * @returns The audio packet that was prepared */ public prepareAudioPacket(opusPacket: Buffer) { const state = this.state; if (state.code !== NetworkingStatusCode.Ready) return; state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData); + // eslint-disable-next-line consistent-return return state.preparedPacket; } @@ -513,6 +518,7 @@ export class Networking extends EventEmitter { state.preparedPacket = undefined; return true; } + return false; } @@ -598,6 +604,7 @@ export class Networking extends EventEmitter { const random = secretbox.methods.random(24, connectionData.nonceBuffer); return [secretbox.methods.close(opusPacket, random, secretKey), random]; } + return [secretbox.methods.close(opusPacket, nonce, secretKey)]; } } diff --git a/packages/voice/src/networking/VoiceUDPSocket.ts b/packages/voice/src/networking/VoiceUDPSocket.ts index 070422b9151b..7a0c77363090 100644 --- a/packages/voice/src/networking/VoiceUDPSocket.ts +++ b/packages/voice/src/networking/VoiceUDPSocket.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/method-signature-style */ -import { createSocket, Socket } from 'node:dgram'; +import { Buffer } from 'node:buffer'; +import { createSocket, type Socket } from 'node:dgram'; import { EventEmitter } from 'node:events'; import { isIPv4 } from 'node:net'; @@ -13,8 +14,8 @@ export interface SocketConfig { } interface KeepAlive { - value: number; timestamp: number; + value: number; } /** @@ -25,7 +26,7 @@ interface KeepAlive { export function parseLocalPacket(message: Buffer): SocketConfig { const packet = Buffer.from(message); - const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf-8'); + const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf8'); if (!isIPv4(ip)) { throw new Error('Malformed IP address'); @@ -100,7 +101,7 @@ export class VoiceUDPSocket extends EventEmitter { /** * The debug logger function, if debugging is enabled. */ - private readonly debug: null | ((message: string) => void); + private readonly debug: ((message: string) => void) | null; /** * Creates a new VoiceUDPSocket. @@ -137,6 +138,7 @@ export class VoiceUDPSocket extends EventEmitter { // Delete all keep alives up to and including the received one this.keepAlives.splice(0, index); } + // Propagate the message this.emit('message', buffer); } @@ -169,7 +171,7 @@ export class VoiceUDPSocket extends EventEmitter { * @param buffer - The buffer to send */ public send(buffer: Buffer) { - return this.socket.send(buffer, this.remote.port, this.remote.ip); + this.socket.send(buffer, this.remote.port, this.remote.ip); } /** @@ -179,6 +181,7 @@ export class VoiceUDPSocket extends EventEmitter { try { this.socket.close(); } catch {} + clearInterval(this.keepAliveInterval); } @@ -187,7 +190,7 @@ export class VoiceUDPSocket extends EventEmitter { * * @param ssrc - The SSRC received from Discord */ - public performIPDiscovery(ssrc: number): Promise { + public async performIPDiscovery(ssrc: number): Promise { return new Promise((resolve, reject) => { const listener = (message: Buffer) => { try { diff --git a/packages/voice/src/networking/VoiceWebSocket.ts b/packages/voice/src/networking/VoiceWebSocket.ts index 1b9ad77aecd0..4c8cd1dd0eab 100644 --- a/packages/voice/src/networking/VoiceWebSocket.ts +++ b/packages/voice/src/networking/VoiceWebSocket.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/method-signature-style */ import { EventEmitter } from 'node:events'; import { VoiceOpcodes } from 'discord-api-types/voice/v4'; -import WebSocket, { MessageEvent } from 'ws'; +import WebSocket, { type MessageEvent } from 'ws'; export interface VoiceWebSocket extends EventEmitter { on(event: 'error', listener: (error: Error) => void): this; @@ -56,7 +56,7 @@ export class VoiceWebSocket extends EventEmitter { /** * The debug logger function, if debugging is enabled. */ - private readonly debug: null | ((message: string) => void); + private readonly debug: ((message: string) => void) | null; /** * The underlying WebSocket of this wrapper. @@ -71,11 +71,11 @@ export class VoiceWebSocket extends EventEmitter { public constructor(address: string, debug: boolean) { super(); this.ws = new WebSocket(address); - this.ws.onmessage = (e) => this.onMessage(e); - this.ws.onopen = (e) => this.emit('open', e); + this.ws.onmessage = (err) => this.onMessage(err); + this.ws.onopen = (err) => this.emit('open', err); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.ws.onerror = (e: Error | WebSocket.ErrorEvent) => this.emit('error', e instanceof Error ? e : e.error); - this.ws.onclose = (e) => this.emit('close', e); + this.ws.onerror = (err: Error | WebSocket.ErrorEvent) => this.emit('error', err instanceof Error ? err : err.error); + this.ws.onclose = (err) => this.emit('close', err); this.lastHeartbeatAck = 0; this.lastHeartbeatSend = 0; @@ -90,10 +90,10 @@ export class VoiceWebSocket extends EventEmitter { try { this.debug?.('destroyed'); this.setHeartbeatInterval(-1); - this.ws.close(1000); + this.ws.close(1_000); } catch (error) { - const e = error as Error; - this.emit('error', e); + const err = error as Error; + this.emit('error', err); } } @@ -113,8 +113,8 @@ export class VoiceWebSocket extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment packet = JSON.parse(event.data); } catch (error) { - const e = error as Error; - this.emit('error', e); + const err = error as Error; + this.emit('error', err); return; } @@ -137,10 +137,11 @@ export class VoiceWebSocket extends EventEmitter { try { const stringified = JSON.stringify(packet); this.debug?.(`>> ${stringified}`); - return this.ws.send(stringified); + this.ws.send(stringified); + return; } catch (error) { - const e = error as Error; - this.emit('error', e); + const err = error as Error; + this.emit('error', err); } } @@ -151,8 +152,9 @@ export class VoiceWebSocket extends EventEmitter { this.lastHeartbeatSend = Date.now(); this.missedHeartbeats++; const nonce = this.lastHeartbeatSend; - return this.sendPacket({ + this.sendPacket({ op: VoiceOpcodes.Heartbeat, + // eslint-disable-next-line id-length d: nonce, }); } @@ -171,6 +173,7 @@ export class VoiceWebSocket extends EventEmitter { this.ws.close(); this.setHeartbeatInterval(-1); } + this.sendHeartbeat(); }, ms); } diff --git a/packages/voice/src/receive/AudioReceiveStream.ts b/packages/voice/src/receive/AudioReceiveStream.ts index 281ffdcfe6ea..ac25a026d75c 100644 --- a/packages/voice/src/receive/AudioReceiveStream.ts +++ b/packages/voice/src/receive/AudioReceiveStream.ts @@ -1,4 +1,5 @@ -import { Readable, ReadableOptions } from 'node:stream'; +import type { Buffer } from 'node:buffer'; +import { Readable, type ReadableOptions } from 'node:stream'; import { SILENCE_FRAME } from '../audio/AudioPlayer'; /** @@ -23,11 +24,11 @@ export enum EndBehaviorType { export type EndBehavior = | { - behavior: EndBehaviorType.Manual; + behavior: EndBehaviorType.AfterInactivity | EndBehaviorType.AfterSilence; + duration: number; } | { - behavior: EndBehaviorType.AfterSilence | EndBehaviorType.AfterInactivity; - duration: number; + behavior: EndBehaviorType.Manual; }; export interface AudioReceiveStreamOptions extends ReadableOptions { @@ -64,14 +65,13 @@ export class AudioReceiveStream extends Readable { } public override push(buffer: Buffer | null) { - if (buffer) { - if ( - this.end.behavior === EndBehaviorType.AfterInactivity || + if ( + buffer && + (this.end.behavior === EndBehaviorType.AfterInactivity || (this.end.behavior === EndBehaviorType.AfterSilence && - (buffer.compare(SILENCE_FRAME) !== 0 || typeof this.endTimeout === 'undefined')) - ) { - this.renewEndTimeout(this.end); - } + (buffer.compare(SILENCE_FRAME) !== 0 || typeof this.endTimeout === 'undefined'))) + ) { + this.renewEndTimeout(this.end); } return super.push(buffer); @@ -81,6 +81,7 @@ export class AudioReceiveStream extends Readable { if (this.endTimeout) { clearTimeout(this.endTimeout); } + this.endTimeout = setTimeout(() => this.push(null), end.duration); } diff --git a/packages/voice/src/receive/SSRCMap.ts b/packages/voice/src/receive/SSRCMap.ts index 1bf8e3630cb5..b6fc11c2cae5 100644 --- a/packages/voice/src/receive/SSRCMap.ts +++ b/packages/voice/src/receive/SSRCMap.ts @@ -11,15 +11,15 @@ export interface VoiceUserData { audioSSRC: number; /** - * The SSRC of the user's video stream (if one exists) - * Cannot be 0. If undefined, the user has no video stream. + * The Discord user id of the user. */ - videoSSRC?: number; + userId: string; /** - * The Discord user id of the user. + * The SSRC of the user's video stream (if one exists) + * Cannot be 0. If undefined, the user has no video stream. */ - userId: string; + videoSSRC?: number; } export interface SSRCMap extends EventEmitter { @@ -83,7 +83,6 @@ export class SSRCMap extends EventEmitter { * Deletes the stored voice data about a user. * * @param target - The target of the delete operation, either their audio SSRC or user id - * * @returns The data that was deleted, if any */ public delete(target: number | string) { @@ -93,6 +92,7 @@ export class SSRCMap extends EventEmitter { this.map.delete(target); this.emit('delete', existing); } + return existing; } diff --git a/packages/voice/src/receive/SpeakingMap.ts b/packages/voice/src/receive/SpeakingMap.ts index b1049d76435c..983355f0adc8 100644 --- a/packages/voice/src/receive/SpeakingMap.ts +++ b/packages/voice/src/receive/SpeakingMap.ts @@ -4,12 +4,14 @@ import { EventEmitter } from 'node:events'; export interface SpeakingMap extends EventEmitter { /** * Emitted when a user starts speaking. + * * @eventProperty */ on(event: 'start', listener: (userId: string) => void): this; /** * Emitted when a user ends speaking. + * * @eventProperty */ on(event: 'end', listener: (userId: string) => void): this; @@ -45,6 +47,7 @@ export class SpeakingMap extends EventEmitter { this.users.set(userId, Date.now()); this.emit('start', userId); } + this.startTimeout(userId); } diff --git a/packages/voice/src/receive/VoiceReceiver.ts b/packages/voice/src/receive/VoiceReceiver.ts index af83426cf57e..f4d21229c601 100644 --- a/packages/voice/src/receive/VoiceReceiver.ts +++ b/packages/voice/src/receive/VoiceReceiver.ts @@ -1,14 +1,16 @@ +/* eslint-disable jsdoc/check-param-names */ +import { Buffer } from 'node:buffer'; import { VoiceOpcodes } from 'discord-api-types/voice/v4'; +import type { VoiceConnection } from '../VoiceConnection'; +import type { ConnectionData } from '../networking/Networking'; +import { methods } from '../util/Secretbox'; import { AudioReceiveStream, - AudioReceiveStreamOptions, createDefaultAudioReceiveStreamOptions, + type AudioReceiveStreamOptions, } from './AudioReceiveStream'; import { SSRCMap } from './SSRCMap'; import { SpeakingMap } from './SpeakingMap'; -import type { VoiceConnection } from '../VoiceConnection'; -import type { ConnectionData } from '../networking/Networking'; -import { methods } from '../util/Secretbox'; /** * Attaches to a VoiceConnection, allowing you to receive audio packets from other @@ -59,7 +61,6 @@ export class VoiceReceiver { * Called when a packet is received on the attached connection's WebSocket. * * @param packet - The received packet - * * @internal */ public onWsPacket(packet: any) { @@ -112,6 +113,7 @@ export class VoiceReceiver { // Open packet const decrypted = methods.open(buffer.slice(12, end), nonce, secretKey); if (!decrypted) return; + // eslint-disable-next-line consistent-return return Buffer.from(decrypted); } @@ -122,7 +124,6 @@ export class VoiceReceiver { * @param mode - The encryption mode * @param nonce - The nonce buffer used by the connection for encryption * @param secretKey - The secret key used by the connection for encryption - * * @returns The parsed Opus packet */ private parsePacket(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array) { @@ -135,6 +136,7 @@ export class VoiceReceiver { packet = packet.subarray(4 + 4 * headerExtensionLength); } + // eslint-disable-next-line consistent-return return packet; } @@ -142,7 +144,6 @@ export class VoiceReceiver { * Called when the UDP socket of the attached connection receives a message. * * @param msg - The received message - * * @internal */ public onUdpMessage(msg: Buffer) { @@ -176,7 +177,6 @@ export class VoiceReceiver { * Creates a subscription for the given user id. * * @param target - The id of the user to subscribe to - * * @returns A readable stream of Opus packets received from the target */ public subscribe(userId: string, options?: Partial) { diff --git a/packages/voice/src/util/Secretbox.ts b/packages/voice/src/util/Secretbox.ts index 435606957461..644cdcf764bc 100644 --- a/packages/voice/src/util/Secretbox.ts +++ b/packages/voice/src/util/Secretbox.ts @@ -1,7 +1,9 @@ +import { Buffer } from 'node:buffer'; + interface Methods { - open: (buffer: Buffer, nonce: Buffer, secretKey: Uint8Array) => Buffer | null; - close: (opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array) => Buffer; - random: (bytes: number, nonce: Buffer) => Buffer; + close(opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer; + open(buffer: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer | null; + random(bytes: number, nonce: Buffer): Buffer; } const libs = { @@ -14,6 +16,7 @@ const libs = { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call if (sodium.crypto_secretbox_open_easy(output, buffer, nonce, secretKey)) return output; } + return null; }, close: (opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array) => { @@ -24,7 +27,7 @@ const libs = { return output; }, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - random: (n: number, buffer: Buffer = Buffer.allocUnsafe(n)) => { + random: (num: number, buffer: Buffer = Buffer.allocUnsafe(num)) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call sodium.randombytes_buf(buffer); return buffer; @@ -36,7 +39,7 @@ const libs = { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access close: sodium.api.crypto_secretbox_easy, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - random: (n: number, buffer: Buffer = Buffer.allocUnsafe(n)) => { + random: (num: number, buffer: Buffer = Buffer.allocUnsafe(num)) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call sodium.api.randombytes_buf(buffer); return buffer; @@ -77,7 +80,7 @@ const methods: Methods = { void (async () => { for (const libName of Object.keys(libs) as (keyof typeof libs)[]) { try { - // eslint-disable-next-line + // eslint-disable-next-line unicorn/no-abusive-eslint-disable, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const lib = require(libName); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; diff --git a/packages/voice/src/util/abortAfter.ts b/packages/voice/src/util/abortAfter.ts index 4dc2fbf3d698..33b1eee62ebe 100644 --- a/packages/voice/src/util/abortAfter.ts +++ b/packages/voice/src/util/abortAfter.ts @@ -6,7 +6,7 @@ export function abortAfter(delay: number): [AbortController, AbortSignal] { const ac = new AbortController(); const timeout = setTimeout(() => ac.abort(), delay); - // @ts-expect-error + // @ts-expect-error: No type for timeout // eslint-disable-next-line @typescript-eslint/no-unsafe-call ac.signal.addEventListener('abort', () => clearTimeout(timeout)); return [ac, ac.signal]; diff --git a/packages/voice/src/util/adapter.ts b/packages/voice/src/util/adapter.ts index 6597f15fbfb3..5cf90bb11217 100644 --- a/packages/voice/src/util/adapter.ts +++ b/packages/voice/src/util/adapter.ts @@ -5,41 +5,40 @@ import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispa * Discord gateway DiscordGatewayAdapters. */ export interface DiscordGatewayAdapterLibraryMethods { + /** + * Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway) + */ + destroy(): void; /** * Call this when you receive a VOICE_SERVER_UPDATE payload that is relevant to the adapter. * * @param data - The inner data of the VOICE_SERVER_UPDATE payload */ - onVoiceServerUpdate: (data: GatewayVoiceServerUpdateDispatchData) => void; + onVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void; /** * Call this when you receive a VOICE_STATE_UPDATE payload that is relevant to the adapter. * * @param data - The inner data of the VOICE_STATE_UPDATE payload */ - onVoiceStateUpdate: (data: GatewayVoiceStateUpdateDispatchData) => void; - /** - * Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway) - */ - destroy: () => void; + onVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void; } /** * Methods that are provided by the implementer of a Discord gateway DiscordGatewayAdapter. */ export interface DiscordGatewayAdapterImplementerMethods { + /** + * This will be called by \@discordjs/voice when the adapter can safely be destroyed as it will no + * longer be used. + */ + destroy(): void; /** * Implement this method such that the given payload is sent to the main Discord gateway connection. * * @param payload - The payload to send to the main Discord gateway connection - * * @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects */ - sendPayload: (payload: any) => boolean; - /** - * This will be called by \@discordjs/voice when the adapter can safely be destroyed as it will no - * longer be used. - */ - destroy: () => void; + sendPayload(payload: any): boolean; } /** diff --git a/packages/voice/src/util/demuxProbe.ts b/packages/voice/src/util/demuxProbe.ts index 3607e51aafe3..55d026d82f4b 100644 --- a/packages/voice/src/util/demuxProbe.ts +++ b/packages/voice/src/util/demuxProbe.ts @@ -1,19 +1,20 @@ +import { Buffer } from 'node:buffer'; +import process from 'node:process'; import { Readable } from 'node:stream'; import prism from 'prism-media'; -import { noop } from './util'; import { StreamType } from '..'; +import { noop } from './util'; /** * Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel. * * @param opusHead - The Opus Head to validate - * * @returns `true` if suitable to play in a Discord voice channel, otherwise `false` */ export function validateDiscordOpusHead(opusHead: Buffer): boolean { const channels = opusHead.readUInt8(9); const sampleRate = opusHead.readUInt32LE(12); - return channels === 2 && sampleRate === 48000; + return channels === 2 && sampleRate === 48_000; } /** @@ -38,22 +39,28 @@ export interface ProbeInfo { * @param stream - The readable stream to probe * @param probeSize - The number of bytes to attempt to read before giving up on the probe * @param validator - The Opus Head validator function - * * @experimental */ -export function demuxProbe( +export async function demuxProbe( stream: Readable, - probeSize = 1024, + probeSize = 1_024, validator = validateDiscordOpusHead, ): Promise { return new Promise((resolve, reject) => { // Preconditions - if (stream.readableObjectMode) return reject(new Error('Cannot probe a readable stream in object mode')); - if (stream.readableEnded) return reject(new Error('Cannot probe a stream that has ended')); + if (stream.readableObjectMode) { + reject(new Error('Cannot probe a readable stream in object mode')); + return; + } + + if (stream.readableEnded) { + reject(new Error('Cannot probe a stream that has ended')); + return; + } let readBuffer = Buffer.alloc(0); - let resolved: StreamType | undefined = undefined; + let resolved: StreamType | undefined; const finish = (type: StreamType) => { // eslint-disable-next-line @typescript-eslint/no-use-before-define @@ -73,6 +80,7 @@ export function demuxProbe( if (readBuffer.length > 0) { stream.push(readBuffer); } + resolve({ stream, type, diff --git a/packages/voice/src/util/entersState.ts b/packages/voice/src/util/entersState.ts index cbc336888ac0..05a7e66ffdde 100644 --- a/packages/voice/src/util/entersState.ts +++ b/packages/voice/src/util/entersState.ts @@ -1,7 +1,7 @@ -import EventEmitter, { once } from 'node:events'; -import { abortAfter } from './abortAfter'; +import { type EventEmitter, once } from 'node:events'; import type { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection'; import type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer'; +import { abortAfter } from './abortAfter'; /** * Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error. @@ -13,7 +13,7 @@ import type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer'; export function entersState( target: VoiceConnection, status: VoiceConnectionStatus, - timeoutOrSignal: number | AbortSignal, + timeoutOrSignal: AbortSignal | number, ): Promise; /** @@ -26,7 +26,7 @@ export function entersState( export function entersState( target: AudioPlayer, status: AudioPlayerStatus, - timeoutOrSignal: number | AbortSignal, + timeoutOrSignal: AbortSignal | number, ): Promise; /** @@ -36,10 +36,10 @@ export function entersState( * @param status - The status that the target should be in * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation */ -export async function entersState( +export async function entersState( target: T, - status: VoiceConnectionStatus | AudioPlayerStatus, - timeoutOrSignal: number | AbortSignal, + status: AudioPlayerStatus | VoiceConnectionStatus, + timeoutOrSignal: AbortSignal | number, ) { if (target.state.status !== status) { const [ac, signal] = @@ -50,5 +50,6 @@ export async function entersState( ac?.abort(); } } + return target; } diff --git a/packages/voice/src/util/generateDependencyReport.ts b/packages/voice/src/util/generateDependencyReport.ts index edb31c6116a8..08d712daf1ff 100644 --- a/packages/voice/src/util/generateDependencyReport.ts +++ b/packages/voice/src/util/generateDependencyReport.ts @@ -24,7 +24,7 @@ function findPackageJSON( if (pkg.name !== packageName) throw new Error('package.json does not match'); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return pkg; - } catch (err) { + } catch { return findPackageJSON(resolve(dir, '..'), packageName, depth - 1); } } @@ -43,7 +43,7 @@ function version(name: string): string { : findPackageJSON(dirname(require.resolve(name)), name, 3); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access return pkg?.version ?? 'not found'; - } catch (err) { + } catch { return 'not found'; } } @@ -81,7 +81,7 @@ export function generateDependencyReport() { const info = prism.FFmpeg.getInfo(); report.push(`- version: ${info.version}`); report.push(`- libopus: ${info.output.includes('--enable-libopus') ? 'yes' : 'no'}`); - } catch (err) { + } catch { report.push('- not found'); } diff --git a/packages/website/.eslintrc.json b/packages/website/.eslintrc.json index 8f5441ba0c85..44d5dfd52cb8 100644 --- a/packages/website/.eslintrc.json +++ b/packages/website/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["marine/prettier/react", "../../.eslintrc.json"], + "extends": ["../../.eslintrc.json", "neon/react", "neon/next", "neon/edge"], "settings": { "react": { "version": "detect" diff --git a/packages/website/package.json b/packages/website/package.json index 62b0ce867e7e..07836bea735a 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -72,8 +72,6 @@ "@types/node": "^16.11.56", "@types/react-dom": "^18.0.6", "@types/react-syntax-highlighter": "^15.5.5", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@unocss/cli": "^0.45.13", "@unocss/preset-web-fonts": "^0.45.13", "@unocss/reset": "^0.45.13", @@ -81,12 +79,7 @@ "@vitest/coverage-c8": "^0.22.1", "concurrently": "^7.3.0", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-react": "^7.31.1", - "eslint-plugin-react-hooks": "^4.6.0", + "eslint-config-neon": "^0.1.23", "happy-dom": "^6.0.4", "prettier": "^2.7.1", "typescript": "^4.8.2", diff --git a/packages/website/src/components/CodeListing.tsx b/packages/website/src/components/CodeListing.tsx index 15c27407da9c..521907ae4459 100644 --- a/packages/website/src/components/CodeListing.tsx +++ b/packages/website/src/components/CodeListing.tsx @@ -37,15 +37,15 @@ export function CodeListing({ deprecation, inheritanceData, }: PropsWithChildren<{ - name: string; - separator?: CodeListingSeparatorType; - typeTokens: TokenDocumentation[]; - readonly?: boolean; - optional?: boolean; - summary?: ApiItemJSON['summary']; comment?: AnyDocNodeJSON | null; deprecation?: AnyDocNodeJSON | null; inheritanceData?: InheritanceData | null; + name: string; + optional?: boolean; + readonly?: boolean; + separator?: CodeListingSeparatorType; + summary?: ApiItemJSON['summary']; + typeTokens: TokenDocumentation[]; }>) { const { classes } = useStyles(); const matches = useMediaQuery('(max-width: 768px)'); diff --git a/packages/website/src/components/DocContainer.tsx b/packages/website/src/components/DocContainer.tsx index 42af51317ec4..934f7880fb19 100644 --- a/packages/website/src/components/DocContainer.tsx +++ b/packages/website/src/components/DocContainer.tsx @@ -2,7 +2,6 @@ import type { ApiItemJSON, TokenDocumentation, TypeParameterData, - AnyDocNodeJSON, ApiClassJSON, ApiInterfaceJSON, } from '@discordjs/api-extractor-utils'; @@ -40,16 +39,15 @@ import { TypeParamTable } from './TypeParamTable'; import { TSDoc } from './tsdoc/TSDoc'; type DocContainerProps = PropsWithChildren<{ - name: string; - kind: string; excerpt: string; - summary?: ApiItemJSON['summary']; extendsTokens?: TokenDocumentation[] | null; implementsTokens?: TokenDocumentation[][]; - typeParams?: TypeParameterData[]; - comment?: AnyDocNodeJSON | null; + kind: string; methods?: ApiClassJSON['methods'] | ApiInterfaceJSON['methods'] | null; + name: string; properties?: ApiClassJSON['properties'] | ApiInterfaceJSON['properties'] | null; + summary?: ApiItemJSON['summary']; + typeParams?: TypeParameterData[]; }>; function generateIcon(kind: string) { @@ -161,7 +159,7 @@ export function DocContainer({ diff --git a/packages/website/src/components/HyperlinkedText.tsx b/packages/website/src/components/HyperlinkedText.tsx index d6953701ed24..bad5e93ed100 100644 --- a/packages/website/src/components/HyperlinkedText.tsx +++ b/packages/website/src/components/HyperlinkedText.tsx @@ -2,13 +2,6 @@ import type { TokenDocumentation } from '@discordjs/api-extractor-utils'; import { Anchor, Text } from '@mantine/core'; import Link from 'next/link'; -/** - * Constructs a hyperlinked html node based on token type references - * - * @param tokens An array of documentation tokens to construct the HTML - * - * @returns An array of JSX elements and string comprising the hyperlinked text - */ export function HyperlinkedText({ tokens }: { tokens: TokenDocumentation[] }) { return ( <> diff --git a/packages/website/src/components/Section.tsx b/packages/website/src/components/Section.tsx index ae17459fe785..68dd8c09f08b 100644 --- a/packages/website/src/components/Section.tsx +++ b/packages/website/src/components/Section.tsx @@ -40,11 +40,11 @@ export function Section({ defaultClosed = false, children, }: PropsWithChildren<{ - title: string; + defaultClosed?: boolean; + dense?: boolean; icon?: JSX.Element; padded?: boolean; - dense?: boolean; - defaultClosed?: boolean; + title: string; }>) { const [opened, setOpened] = useState(!defaultClosed); const { colorScheme } = useMantineColorScheme(); @@ -57,7 +57,7 @@ export function Section({ return ( - setOpened((o) => !o)}> + setOpened((isOpen) => !isOpen)}> {icon ? ( diff --git a/packages/website/src/components/SidebarItems.tsx b/packages/website/src/components/SidebarItems.tsx index a0bd16bb4eeb..6d15a63788cb 100644 --- a/packages/website/src/components/SidebarItems.tsx +++ b/packages/website/src/components/SidebarItems.tsx @@ -61,7 +61,7 @@ function resolveIcon(item: keyof GroupedMembers) { return ; case 'Variables': return ; - case 'Functions': + default: return ; } } @@ -115,12 +115,12 @@ export function SidebarItems({ .filter((group) => groupItems[group].length) .map((group, idx) => (
- {groupItems[group].map((member, i) => ( - + {groupItems[group].map((member, index) => ( + setOpened((o) => !o)} + onClick={() => setOpened((isOpened) => !isOpened)} label={ @@ -135,7 +135,7 @@ export function SidebarItems({ } active={asPathWithoutQueryAndAnchor === member.path} variant="filled" - > + /> ))}
diff --git a/packages/website/src/components/SidebarLayout.tsx b/packages/website/src/components/SidebarLayout.tsx index 0bd9053ee819..96d7550003e5 100644 --- a/packages/website/src/components/SidebarLayout.tsx +++ b/packages/website/src/components/SidebarLayout.tsx @@ -25,10 +25,10 @@ import { Title, } from '@mantine/core'; import { NextLink } from '@mantine/next'; -import type { MDXRemoteSerializeResult } from 'next-mdx-remote'; import Image from 'next/future/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import type { MDXRemoteSerializeResult } from 'next-mdx-remote'; import { type PropsWithChildren, useState, useEffect, useMemo } from 'react'; import { VscChevronDown, VscGithubInverted, VscPackage, VscVersions } from 'react-icons/vsc'; import { WiDaySunny, WiNightClear } from 'react-icons/wi'; @@ -37,16 +37,19 @@ import { SidebarItems } from './SidebarItems'; import type { findMember } from '~/util/model.server'; import { PACKAGES } from '~/util/packages'; -const fetcher = (url: string) => fetch(url).then((res) => res.json()); +const fetcher = async (url: string) => { + const res = await fetch(url); + return res.json(); +}; export interface SidebarLayoutProps { - packageName: string; branchName: string; data: { - members: ReturnType; member: ReturnType; + members: ReturnType; source: MDXRemoteSerializeResult; }; + packageName: string; selectedMember?: ApiItemJSON | undefined; } @@ -55,8 +58,8 @@ export type Members = SidebarLayoutProps['data']['members']; export interface GroupedMembers { Classes: Members; - Functions: Members; Enums: Members; + Functions: Members; Interfaces: Members; Types: Members; Variables: Members; @@ -158,6 +161,7 @@ export function SidebarLayout({ fetcher, ); const theme = useMantineTheme(); + // eslint-disable-next-line @typescript-eslint/unbound-method const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const [opened, setOpened] = useState(false); @@ -188,7 +192,7 @@ export function SidebarLayout({ {item} )) ?? [], - [versions], + [versions, packageName], ); const breadcrumbs = useMemo( @@ -302,7 +306,7 @@ export function SidebarLayout({ (router.isFallback ? null : setOpened((o) => !o))} + onClick={() => (router.isFallback ? null : setOpened((isOpened) => !isOpened))} size="sm" color={theme.colors.gray![6]} mr="xl" @@ -346,7 +350,7 @@ export function SidebarLayout({ {children} - ({ height: 200, [theme.fn.smallerThan('sm')]: { height: 300 } })}> + ({ height: 200, [theme.fn.smallerThan('sm')]: { height: 300 } })} /> ; + columns: string[]; rows: Record[]; }) { return ( diff --git a/packages/website/src/components/TableOfContentItems.tsx b/packages/website/src/components/TableOfContentItems.tsx index ba3d7dd11aca..5651c9185b12 100644 --- a/packages/website/src/components/TableOfContentItems.tsx +++ b/packages/website/src/components/TableOfContentItems.tsx @@ -48,6 +48,7 @@ export function TableOfContentItems({
)), + // eslint-disable-next-line react-hooks/exhaustive-deps [properties, colorScheme], ); @@ -73,7 +74,7 @@ export function TableOfContentItems({ ); }), - [methods, colorScheme], + [methods, classes.link], ); return ( diff --git a/packages/website/src/components/model/Class.tsx b/packages/website/src/components/model/Class.tsx index ffc001b78d7f..24d080930a19 100644 --- a/packages/website/src/components/model/Class.tsx +++ b/packages/website/src/components/model/Class.tsx @@ -16,7 +16,6 @@ export function Class({ data }: { data: ApiClassJSON }) { typeParams={data.typeParameters} extendsTokens={data.extendsTokens} implementsTokens={data.implementsTokens} - comment={data.comment} methods={data.methods} properties={data.properties} > diff --git a/packages/website/src/components/tsdoc/BlockComment.tsx b/packages/website/src/components/tsdoc/BlockComment.tsx index 31575ef72fff..729d2c4b1b2b 100644 --- a/packages/website/src/components/tsdoc/BlockComment.tsx +++ b/packages/website/src/components/tsdoc/BlockComment.tsx @@ -40,8 +40,8 @@ export function BlockComment({ tagName, index, }: PropsWithChildren<{ - tagName: string; index?: number | undefined; + tagName: string; }>): JSX.Element { switch (tagName.toUpperCase()) { case StandardTags.example.tagNameWithUpperCase: diff --git a/packages/website/src/components/tsdoc/TSDoc.tsx b/packages/website/src/components/tsdoc/TSDoc.tsx index cae45400a58f..3d81b4a35f93 100644 --- a/packages/website/src/components/tsdoc/TSDoc.tsx +++ b/packages/website/src/components/tsdoc/TSDoc.tsx @@ -63,6 +63,7 @@ export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element { return null; } + case DocNodeKind.CodeSpan: { const { code } = node as DocFencedCodeJSON; return ( @@ -71,6 +72,7 @@ export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element { ); } + case DocNodeKind.FencedCode: { const { language, code } = node as DocFencedCodeJSON; return ( @@ -86,6 +88,7 @@ export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element { ); } + case DocNodeKind.ParamBlock: case DocNodeKind.Block: { const { tag } = node as DocBlockJSON; @@ -102,6 +105,7 @@ export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element { ); } + case DocNodeKind.Comment: { const comment = node as DocCommentJSON; // Cheat a bit by finding out how many comments we have beforehand... @@ -111,6 +115,7 @@ export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element { return {comment.customBlocks.map((node, idx) => createNode(node, idx))}; } + default: console.log(`Captured unknown node kind: ${node.kind}`); break; diff --git a/packages/website/src/contexts/member.tsx b/packages/website/src/contexts/member.tsx index 5c15056d2d42..5dd8990262d8 100644 --- a/packages/website/src/contexts/member.tsx +++ b/packages/website/src/contexts/member.tsx @@ -3,7 +3,7 @@ import { createContext, useContext, type ReactNode } from 'react'; export const MemberContext = createContext(undefined); -export const MemberProvider = ({ member, children }: { member: ApiItemJSON | undefined; children: ReactNode }) => ( +export const MemberProvider = ({ member, children }: { children: ReactNode; member: ApiItemJSON | undefined }) => ( {children} ); diff --git a/packages/website/src/middleware.ts b/packages/website/src/middleware.ts index 77035fe0e718..e11cc3f806cc 100644 --- a/packages/website/src/middleware.ts +++ b/packages/website/src/middleware.ts @@ -1,5 +1,4 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; +import { NextResponse, type NextRequest } from 'next/server'; export default function middleware(request: NextRequest) { return NextResponse.redirect(new URL('/docs/packages', request.url)); diff --git a/packages/website/src/pages/_app.tsx b/packages/website/src/pages/_app.tsx index b66b00d57096..cfee630283c8 100644 --- a/packages/website/src/pages/_app.tsx +++ b/packages/website/src/pages/_app.tsx @@ -5,7 +5,7 @@ import type { AppProps } from 'next/app'; import Head from 'next/head'; import { type NextRouter, useRouter } from 'next/router'; import { VscPackage } from 'react-icons/vsc'; -import { RouterTransition } from '~/components/RouterTransition'; +import { RouterTransition } from '../components/RouterTransition'; import '../styles/unocss.css'; import '../styles/main.css'; diff --git a/packages/website/src/pages/docs/[...slug].tsx b/packages/website/src/pages/docs/[...slug].tsx index 3b8337f86afb..a1bf8d0e9fb2 100644 --- a/packages/website/src/pages/docs/[...slug].tsx +++ b/packages/website/src/pages/docs/[...slug].tsx @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { cwd } from 'node:process'; +import process, { cwd } from 'node:process'; import { findPackage, getMembers, @@ -15,11 +15,11 @@ import { import { ActionIcon, Affix, Box, LoadingOverlay, Transition } from '@mantine/core'; import { useMediaQuery, useWindowScroll } from '@mantine/hooks'; import { ApiFunction, ApiItemKind, type ApiPackage } from '@microsoft/api-extractor-model'; -import { MDXRemote } from 'next-mdx-remote'; -import { serialize } from 'next-mdx-remote/serialize'; import Head from 'next/head'; import { useRouter } from 'next/router'; import type { GetStaticPaths, GetStaticProps } from 'next/types'; +import { MDXRemote } from 'next-mdx-remote'; +import { serialize } from 'next-mdx-remote/serialize'; import { VscChevronUp } from 'react-icons/vsc'; import rehypeIgnore from 'rehype-ignore'; import rehypePrettyCode from 'rehype-pretty-code'; @@ -46,7 +46,7 @@ export const getStaticPaths: GetStaticPaths = async () => { let data: any[] = []; let versions: string[] = []; if (process.env.NEXT_PUBLIC_LOCAL_DEV) { - const res = await readFile(join(cwd(), '..', packageName, 'docs', 'docs.api.json'), 'utf-8'); + const res = await readFile(join(cwd(), '..', packageName, 'docs', 'docs.api.json'), 'utf8'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data = JSON.parse(res); } else { @@ -62,35 +62,33 @@ export const getStaticPaths: GetStaticPaths = async () => { } if (Array.isArray(data)) { - const models = data.map((d) => createApiModel(d)); + const models = data.map((innerData) => createApiModel(innerData)); const pkgs = models.map((model) => findPackage(model, packageName)) as ApiPackage[]; return [ ...versions.map((version) => ({ params: { slug: ['packages', packageName, version] } })), - ...pkgs - .map((pkg, idx) => - getMembers(pkg, versions[idx]!).map((member) => { - if (member.kind === ApiItemKind.Function && member.overloadIndex && member.overloadIndex > 1) { - return { - params: { - slug: [ - 'packages', - packageName, - versions[idx]!, - `${member.name}:${member.overloadIndex}:${member.kind}`, - ], - }, - }; - } - + ...pkgs.flatMap((pkg, idx) => + getMembers(pkg, versions[idx]!).map((member) => { + if (member.kind === ApiItemKind.Function && member.overloadIndex && member.overloadIndex > 1) { return { params: { - slug: ['packages', packageName, versions[idx]!, `${member.name}:${member.kind}`], + slug: [ + 'packages', + packageName, + versions[idx]!, + `${member.name}:${member.overloadIndex}:${member.kind}`, + ], }, }; - }), - ) - .flat(), + } + + return { + params: { + slug: ['packages', packageName, versions[idx]!, `${member.name}:${member.kind}`], + }, + }; + }), + ), ]; } @@ -130,7 +128,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { const [memberName, overloadIndex] = member?.split(':') ?? []; try { - const readme = await readFile(join(cwd(), '..', packageName, 'README.md'), 'utf-8'); + const readme = await readFile(join(cwd(), '..', packageName, 'README.md'), 'utf8'); const mdxSource = await serialize(readme, { mdxOptions: { @@ -143,7 +141,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { let data; if (process.env.NEXT_PUBLIC_LOCAL_DEV) { - const res = await readFile(join(cwd(), '..', packageName, 'docs', 'docs.api.json'), 'utf-8'); + const res = await readFile(join(cwd(), '..', packageName, 'docs', 'docs.api.json'), 'utf8'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data = JSON.parse(res); } else { @@ -155,9 +153,10 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { const model = createApiModel(data); const pkg = findPackage(model, packageName); + // eslint-disable-next-line prefer-const let { containerKey, name } = findMember(model, packageName, memberName, branchName) ?? {}; - if (name && overloadIndex && !Number.isNaN(parseInt(overloadIndex, 10))) { - containerKey = ApiFunction.getContainerKey(name, parseInt(overloadIndex, 10)); + if (name && overloadIndex && !Number.isNaN(Number.parseInt(overloadIndex, 10))) { + containerKey = ApiFunction.getContainerKey(name, Number.parseInt(overloadIndex, 10)); } return { @@ -171,17 +170,17 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { source: mdxSource, }, }, - revalidate: 3600, + revalidate: 3_600, }; - } catch (e) { - const error = e as Error; + } catch (error_) { + const error = error_ as Error; console.error(error); return { props: { - error: e, + error: error_, }, - revalidate: 3600, + revalidate: 3_600, }; } }; diff --git a/packages/website/src/pages/docs/packages/[package]/index.tsx b/packages/website/src/pages/docs/packages/[package]/index.tsx index 668a6f86ba88..6f6bbaf8945d 100644 --- a/packages/website/src/pages/docs/packages/[package]/index.tsx +++ b/packages/website/src/pages/docs/packages/[package]/index.tsx @@ -18,10 +18,10 @@ import { VscArrowLeft, VscArrowRight, VscVersions } from 'react-icons/vsc'; import { PACKAGES } from '~/util/packages'; interface VersionProps { - packageName: string; data: { versions: string[]; }; + packageName: string; } export const getStaticPaths: GetStaticPaths = () => { @@ -48,7 +48,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { props: { error: 'No tags', }, - revalidate: 3600, + revalidate: 3_600, }; } @@ -59,17 +59,17 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { versions: data, }, }, - revalidate: 3600, + revalidate: 3_600, }; - } catch (e) { - const error = e as Error; + } catch (error_) { + const error = error_ as Error; console.error(error); return { props: { - error: e, + error: error_, }, - revalidate: 3600, + revalidate: 3_600, }; } }; diff --git a/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts b/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts index b0294c3eeb04..7ce3de334ebc 100644 --- a/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts +++ b/packages/ws/__tests__/strategy/WorkerContextFetchingStrategy.test.ts @@ -1,15 +1,16 @@ +/* eslint-disable @typescript-eslint/consistent-type-imports */ import { REST } from '@discordjs/rest'; -import { MockAgent, Interceptable } from 'undici'; +import { MockAgent, type Interceptable } from 'undici'; import { beforeEach, test, vi, expect } from 'vitest'; import { managerToFetchingStrategyOptions, WorkerContextFetchingStrategy, - WorkerRecievePayload, - WorkerSendPayload, WebSocketManager, WorkerSendPayloadOp, WorkerRecievePayloadOp, -} from '../../src'; + type WorkerRecievePayload, + type WorkerSendPayload, +} from '../../src/index.js'; let mockAgent: MockAgent; let mockPool: Interceptable; diff --git a/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts b/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts index b0c18fc286f1..69ace627b76e 100644 --- a/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts +++ b/packages/ws/__tests__/strategy/WorkerShardingStrategy.test.ts @@ -1,22 +1,24 @@ +/* eslint-disable id-length */ +import { setImmediate } from 'node:timers'; import { REST } from '@discordjs/rest'; import { GatewayDispatchEvents, - GatewayDispatchPayload, GatewayOpcodes, - GatewaySendPayload, + type GatewayDispatchPayload, + type GatewaySendPayload, } from 'discord-api-types/v10'; -import { MockAgent, Interceptable } from 'undici'; +import { MockAgent, type Interceptable } from 'undici'; import { beforeEach, test, vi, expect, afterEach } from 'vitest'; import { - WorkerRecievePayload, - WorkerSendPayload, WebSocketManager, WorkerSendPayloadOp, WorkerRecievePayloadOp, WorkerShardingStrategy, WebSocketShardEvents, - SessionInfo, -} from '../../src'; + type WorkerRecievePayload, + type WorkerSendPayload, + type SessionInfo, +} from '../../src/index.js'; let mockAgent: MockAgent; let mockPool: Interceptable; @@ -43,6 +45,7 @@ const sessionInfo: SessionInfo = { }; vi.mock('node:worker_threads', async () => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports const { EventEmitter }: typeof import('node:events') = await vi.importActual('node:events'); class MockWorker extends EventEmitter { public constructor(...args: any[]) { @@ -54,6 +57,7 @@ vi.mock('node:worker_threads', async () => { } public postMessage(message: WorkerSendPayload) { + // eslint-disable-next-line default-case switch (message.op) { case WorkerSendPayloadOp.Connect: { const response: WorkerRecievePayload = { diff --git a/packages/ws/__tests__/util/IdentifyThrottler.test.ts b/packages/ws/__tests__/util/IdentifyThrottler.test.ts index 591263f5ad5e..74417d5881eb 100644 --- a/packages/ws/__tests__/util/IdentifyThrottler.test.ts +++ b/packages/ws/__tests__/util/IdentifyThrottler.test.ts @@ -1,6 +1,6 @@ import { setTimeout as sleep } from 'node:timers/promises'; -import { expect, Mock, test, vi } from 'vitest'; -import { IdentifyThrottler, WebSocketManager } from '../../src'; +import { expect, test, vi, type Mock } from 'vitest'; +import { IdentifyThrottler, type WebSocketManager } from '../../src/index.js'; vi.mock('node:timers/promises', () => ({ setTimeout: vi.fn(), diff --git a/packages/ws/__tests__/ws/WebSocketManager.test.ts b/packages/ws/__tests__/ws/WebSocketManager.test.ts index ba3a75a08e24..fad53ae1c39b 100644 --- a/packages/ws/__tests__/ws/WebSocketManager.test.ts +++ b/packages/ws/__tests__/ws/WebSocketManager.test.ts @@ -1,8 +1,8 @@ import { REST } from '@discordjs/rest'; -import { APIGatewayBotInfo, GatewayOpcodes, GatewaySendPayload } from 'discord-api-types/v10'; -import { MockAgent, Interceptable } from 'undici'; +import { GatewayOpcodes, type APIGatewayBotInfo, type GatewaySendPayload } from 'discord-api-types/v10'; +import { MockAgent, type Interceptable } from 'undici'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { IShardingStrategy, WebSocketManager } from '../../src'; +import { WebSocketManager, type IShardingStrategy } from '../../src/index.js'; vi.useFakeTimers(); @@ -80,7 +80,7 @@ test('fetch gateway information', async () => { }) .reply(fetch); - NOW.mockReturnValue(Infinity); + NOW.mockReturnValue(Number.POSITIVE_INFINITY); const cacheExpired = await manager.fetchGatewayInformation(); expect(cacheExpired).toEqual(data); expect(fetch).toHaveBeenCalledOnce(); @@ -171,8 +171,11 @@ test('it handles passing in both shardIds and shardCount', async () => { test('strategies', async () => { class MockStrategy implements IShardingStrategy { public spawn = vi.fn(); + public connect = vi.fn(); + public destroy = vi.fn(); + public send = vi.fn(); } @@ -219,6 +222,7 @@ test('strategies', async () => { await manager.destroy(destroyOptions); expect(strategy.destroy).toHaveBeenCalledWith(destroyOptions); + // eslint-disable-next-line id-length const send: GatewaySendPayload = { op: GatewayOpcodes.RequestGuildMembers, d: { guild_id: '1234', limit: 0 } }; await manager.send(0, send); expect(strategy.send).toHaveBeenCalledWith(0, send); diff --git a/packages/ws/package.json b/packages/ws/package.json index 0cd93832a92c..7be8d2199fcf 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -66,16 +66,10 @@ "@favware/cliff-jumper": "^1.8.7", "@microsoft/api-extractor": "^7.29.5", "@types/node": "^16.11.56", - "@typescript-eslint/eslint-plugin": "^5.36.1", - "@typescript-eslint/parser": "^5.36.1", "@vitest/coverage-c8": "^0.22.1", "downlevel-dts": "^0.10.1", "eslint": "^8.23.0", - "eslint-config-marine": "^9.4.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-typescript": "^3.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-tsdoc": "^0.2.16", + "eslint-config-neon": "^0.1.23", "mock-socket": "^9.1.5", "prettier": "^2.7.1", "rollup-plugin-typescript2": "^0.33.0", diff --git a/packages/ws/src/index.ts b/packages/ws/src/index.ts index 947236e558b0..2627bd9da31f 100644 --- a/packages/ws/src/index.ts +++ b/packages/ws/src/index.ts @@ -1,14 +1,14 @@ -export * from './strategies/context/IContextFetchingStrategy'; -export * from './strategies/context/SimpleContextFetchingStrategy'; -export * from './strategies/context/WorkerContextFetchingStrategy'; +export * from './strategies/context/IContextFetchingStrategy.js'; +export * from './strategies/context/SimpleContextFetchingStrategy.js'; +export * from './strategies/context/WorkerContextFetchingStrategy.js'; -export * from './strategies/sharding/IShardingStrategy'; -export * from './strategies/sharding/SimpleShardingStrategy'; -export * from './strategies/sharding/WorkerShardingStrategy'; +export * from './strategies/sharding/IShardingStrategy.js'; +export * from './strategies/sharding/SimpleShardingStrategy.js'; +export * from './strategies/sharding/WorkerShardingStrategy.js'; -export * from './utils/constants'; -export * from './utils/IdentifyThrottler'; -export * from './utils/utils'; +export * from './utils/constants.js'; +export * from './utils/IdentifyThrottler.js'; +export * from './utils/utils.js'; -export * from './ws/WebSocketManager'; -export * from './ws/WebSocketShard'; +export * from './ws/WebSocketManager.js'; +export * from './ws/WebSocketShard.js'; diff --git a/packages/ws/src/strategies/context/IContextFetchingStrategy.ts b/packages/ws/src/strategies/context/IContextFetchingStrategy.ts index 8fc201921b43..d63c36a2b06b 100644 --- a/packages/ws/src/strategies/context/IContextFetchingStrategy.ts +++ b/packages/ws/src/strategies/context/IContextFetchingStrategy.ts @@ -5,7 +5,7 @@ import type { SessionInfo, WebSocketManager, WebSocketManagerOptions } from '../ export interface FetchingStrategyOptions extends Omit< WebSocketManagerOptions, - 'retrieveSessionInfo' | 'updateSessionInfo' | 'shardCount' | 'shardIds' | 'rest' + 'rest' | 'retrieveSessionInfo' | 'shardCount' | 'shardIds' | 'updateSessionInfo' > { readonly gatewayInformation: APIGatewayBotInfo; readonly shardCount: number; @@ -16,11 +16,12 @@ export interface FetchingStrategyOptions */ export interface IContextFetchingStrategy { readonly options: FetchingStrategyOptions; - retrieveSessionInfo: (shardId: number) => Awaitable; - updateSessionInfo: (shardId: number, sessionInfo: SessionInfo | null) => Awaitable; + retrieveSessionInfo(shardId: number): Awaitable; + updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable; } export async function managerToFetchingStrategyOptions(manager: WebSocketManager): Promise { + // eslint-disable-next-line @typescript-eslint/unbound-method const { retrieveSessionInfo, updateSessionInfo, shardCount, shardIds, rest, ...managerOptions } = manager.options; return { diff --git a/packages/ws/src/strategies/context/SimpleContextFetchingStrategy.ts b/packages/ws/src/strategies/context/SimpleContextFetchingStrategy.ts index 642ef6ce1b02..9a942dd4b1d7 100644 --- a/packages/ws/src/strategies/context/SimpleContextFetchingStrategy.ts +++ b/packages/ws/src/strategies/context/SimpleContextFetchingStrategy.ts @@ -1,5 +1,5 @@ -import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy'; -import type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager'; +import type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager.js'; +import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy.js'; export class SimpleContextFetchingStrategy implements IContextFetchingStrategy { public constructor(private readonly manager: WebSocketManager, public readonly options: FetchingStrategyOptions) {} diff --git a/packages/ws/src/strategies/context/WorkerContextFetchingStrategy.ts b/packages/ws/src/strategies/context/WorkerContextFetchingStrategy.ts index ed358a151242..5ada996f1dda 100644 --- a/packages/ws/src/strategies/context/WorkerContextFetchingStrategy.ts +++ b/packages/ws/src/strategies/context/WorkerContextFetchingStrategy.ts @@ -1,13 +1,13 @@ import { isMainThread, parentPort } from 'node:worker_threads'; import { Collection } from '@discordjs/collection'; -import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy'; -import type { SessionInfo } from '../../ws/WebSocketManager'; +import type { SessionInfo } from '../../ws/WebSocketManager.js'; import { - WorkerRecievePayload, WorkerRecievePayloadOp, - WorkerSendPayload, WorkerSendPayloadOp, -} from '../sharding/WorkerShardingStrategy'; + type WorkerRecievePayload, + type WorkerSendPayload, +} from '../sharding/WorkerShardingStrategy.js'; +import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy.js'; export class WorkerContextFetchingStrategy implements IContextFetchingStrategy { private readonly sessionPromises = new Collection void>(); @@ -33,7 +33,9 @@ export class WorkerContextFetchingStrategy implements IContextFetchingStrategy { shardId, nonce, }; + // eslint-disable-next-line no-promise-executor-return const promise = new Promise((resolve) => this.sessionPromises.set(nonce, resolve)); + // eslint-disable-next-line unicorn/require-post-message-target-origin parentPort!.postMessage(payload); return promise; } @@ -44,6 +46,7 @@ export class WorkerContextFetchingStrategy implements IContextFetchingStrategy { shardId, session: sessionInfo, }; + // eslint-disable-next-line unicorn/require-post-message-target-origin parentPort!.postMessage(payload); } } diff --git a/packages/ws/src/strategies/sharding/IShardingStrategy.ts b/packages/ws/src/strategies/sharding/IShardingStrategy.ts index 7fcf2667295c..911cc9a8c089 100644 --- a/packages/ws/src/strategies/sharding/IShardingStrategy.ts +++ b/packages/ws/src/strategies/sharding/IShardingStrategy.ts @@ -6,20 +6,20 @@ import type { WebSocketShardDestroyOptions } from '../../ws/WebSocketShard'; * Strategies responsible for spawning, initializing connections, destroying shards, and relaying events */ export interface IShardingStrategy { - /** - * Spawns all the shards - */ - spawn: (shardIds: number[]) => Awaitable; /** * Initializes all the shards */ - connect: () => Awaitable; + connect(): Awaitable; /** * Destroys all the shards */ - destroy: (options?: Omit) => Awaitable; + destroy(options?: Omit): Awaitable; /** * Sends a payload to a shard */ - send: (shardId: number, payload: GatewaySendPayload) => Awaitable; + send(shardId: number, payload: GatewaySendPayload): Awaitable; + /** + * Spawns all the shards + */ + spawn(shardIds: number[]): Awaitable; } diff --git a/packages/ws/src/strategies/sharding/SimpleShardingStrategy.ts b/packages/ws/src/strategies/sharding/SimpleShardingStrategy.ts index c6c6b6bbed5a..01276c17575b 100644 --- a/packages/ws/src/strategies/sharding/SimpleShardingStrategy.ts +++ b/packages/ws/src/strategies/sharding/SimpleShardingStrategy.ts @@ -1,17 +1,18 @@ import { Collection } from '@discordjs/collection'; import type { GatewaySendPayload } from 'discord-api-types/v10'; -import type { IShardingStrategy } from './IShardingStrategy'; -import { IdentifyThrottler } from '../../utils/IdentifyThrottler'; +import { IdentifyThrottler } from '../../utils/IdentifyThrottler.js'; import type { WebSocketManager } from '../../ws/WebSocketManager'; -import { WebSocketShard, WebSocketShardDestroyOptions, WebSocketShardEvents } from '../../ws/WebSocketShard'; -import { managerToFetchingStrategyOptions } from '../context/IContextFetchingStrategy'; -import { SimpleContextFetchingStrategy } from '../context/SimpleContextFetchingStrategy'; +import { WebSocketShard, WebSocketShardEvents, type WebSocketShardDestroyOptions } from '../../ws/WebSocketShard.js'; +import { managerToFetchingStrategyOptions } from '../context/IContextFetchingStrategy.js'; +import { SimpleContextFetchingStrategy } from '../context/SimpleContextFetchingStrategy.js'; +import type { IShardingStrategy } from './IShardingStrategy.js'; /** * Simple strategy that just spawns shards in the current process */ export class SimpleShardingStrategy implements IShardingStrategy { private readonly manager: WebSocketManager; + private readonly shards = new Collection(); private readonly throttler: IdentifyThrottler; @@ -30,9 +31,10 @@ export class SimpleShardingStrategy implements IShardingStrategy { const strategy = new SimpleContextFetchingStrategy(this.manager, strategyOptions); const shard = new WebSocketShard(strategy, shardId); for (const event of Object.values(WebSocketShardEvents)) { - // @ts-expect-error + // @ts-expect-error: Intentional shard.on(event, (payload) => this.manager.emit(event, { ...payload, shardId })); } + this.shards.set(shardId, shard); } } @@ -68,7 +70,7 @@ export class SimpleShardingStrategy implements IShardingStrategy { /** * {@inheritDoc IShardingStrategy.send} */ - public send(shardId: number, payload: GatewaySendPayload) { + public async send(shardId: number, payload: GatewaySendPayload) { const shard = this.shards.get(shardId); if (!shard) throw new Error(`Shard ${shardId} not found`); return shard.send(payload); diff --git a/packages/ws/src/strategies/sharding/WorkerShardingStrategy.ts b/packages/ws/src/strategies/sharding/WorkerShardingStrategy.ts index e15c4a5dd8b7..bfc8f2436921 100644 --- a/packages/ws/src/strategies/sharding/WorkerShardingStrategy.ts +++ b/packages/ws/src/strategies/sharding/WorkerShardingStrategy.ts @@ -3,11 +3,11 @@ import { join } from 'node:path'; import { Worker } from 'node:worker_threads'; import { Collection } from '@discordjs/collection'; import type { GatewaySendPayload } from 'discord-api-types/v10'; -import type { IShardingStrategy } from './IShardingStrategy'; -import { IdentifyThrottler } from '../../utils/IdentifyThrottler'; +import { IdentifyThrottler } from '../../utils/IdentifyThrottler.js'; import type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager'; import type { WebSocketShardDestroyOptions, WebSocketShardEvents } from '../../ws/WebSocketShard'; -import { FetchingStrategyOptions, managerToFetchingStrategyOptions } from '../context/IContextFetchingStrategy'; +import { managerToFetchingStrategyOptions, type FetchingStrategyOptions } from '../context/IContextFetchingStrategy.js'; +import type { IShardingStrategy } from './IShardingStrategy.js'; export interface WorkerData extends FetchingStrategyOptions { shardIds: number[]; @@ -21,10 +21,10 @@ export enum WorkerSendPayloadOp { } export type WorkerSendPayload = + | { nonce: number; op: WorkerSendPayloadOp.SessionInfoResponse; session: SessionInfo | null } | { op: WorkerSendPayloadOp.Connect; shardId: number } - | { op: WorkerSendPayloadOp.Destroy; shardId: number; options?: WebSocketShardDestroyOptions } - | { op: WorkerSendPayloadOp.Send; shardId: number; payload: GatewaySendPayload } - | { op: WorkerSendPayloadOp.SessionInfoResponse; nonce: number; session: SessionInfo | null }; + | { op: WorkerSendPayloadOp.Destroy; options?: WebSocketShardDestroyOptions; shardId: number } + | { op: WorkerSendPayloadOp.Send; payload: GatewaySendPayload; shardId: number }; export enum WorkerRecievePayloadOp { Connected, @@ -35,12 +35,12 @@ export enum WorkerRecievePayloadOp { } export type WorkerRecievePayload = + // Can't seem to get a type-safe union based off of the event, so I'm sadly leaving data as any for now + | { data: any; event: WebSocketShardEvents; op: WorkerRecievePayloadOp.Event; shardId: number } + | { nonce: number; op: WorkerRecievePayloadOp.RetrieveSessionInfo; shardId: number } | { op: WorkerRecievePayloadOp.Connected; shardId: number } | { op: WorkerRecievePayloadOp.Destroyed; shardId: number } - // Can't seem to get a type-safe union based off of the event, so I'm sadly leaving data as any for now - | { op: WorkerRecievePayloadOp.Event; shardId: number; event: WebSocketShardEvents; data: any } - | { op: WorkerRecievePayloadOp.RetrieveSessionInfo; shardId: number; nonce: number } - | { op: WorkerRecievePayloadOp.UpdateSessionInfo; shardId: number; session: SessionInfo | null }; + | { op: WorkerRecievePayloadOp.UpdateSessionInfo; session: SessionInfo | null; shardId: number }; /** * Options for a {@link WorkerShardingStrategy} @@ -57,12 +57,15 @@ export interface WorkerShardingStrategyOptions { */ export class WorkerShardingStrategy implements IShardingStrategy { private readonly manager: WebSocketManager; + private readonly options: WorkerShardingStrategyOptions; #workers: Worker[] = []; + readonly #workerByShardId = new Collection(); private readonly connectPromises = new Collection void>(); + private readonly destroyPromises = new Collection void>(); private readonly throttler: IdentifyThrottler; @@ -98,7 +101,7 @@ export class WorkerShardingStrategy implements IShardingStrategy { throw err; }) // eslint-disable-next-line @typescript-eslint/no-misused-promises - .on('message', (payload: WorkerRecievePayload) => this.onMessage(worker, payload)); + .on('message', async (payload: WorkerRecievePayload) => this.onMessage(worker, payload)); this.#workers.push(worker); for (const shardId of slice) { @@ -123,7 +126,9 @@ export class WorkerShardingStrategy implements IShardingStrategy { shardId, }; + // eslint-disable-next-line no-promise-executor-return const promise = new Promise((resolve) => this.connectPromises.set(shardId, resolve)); + // eslint-disable-next-line unicorn/require-post-message-target-origin worker.postMessage(payload); promises.push(promise); } @@ -145,8 +150,10 @@ export class WorkerShardingStrategy implements IShardingStrategy { }; promises.push( - new Promise((resolve) => this.destroyPromises.set(shardId, resolve)).then(() => worker.terminate()), + // eslint-disable-next-line no-promise-executor-return, promise/prefer-await-to-then + new Promise((resolve) => this.destroyPromises.set(shardId, resolve)).then(async () => worker.terminate()), ); + // eslint-disable-next-line unicorn/require-post-message-target-origin worker.postMessage(payload); } @@ -170,10 +177,12 @@ export class WorkerShardingStrategy implements IShardingStrategy { shardId, payload: data, }; + // eslint-disable-next-line unicorn/require-post-message-target-origin worker.postMessage(payload); } private async onMessage(worker: Worker, payload: WorkerRecievePayload) { + // eslint-disable-next-line default-case switch (payload.op) { case WorkerRecievePayloadOp.Connected: { const resolve = this.connectPromises.get(payload.shardId)!; @@ -202,6 +211,7 @@ export class WorkerShardingStrategy implements IShardingStrategy { nonce: payload.nonce, session, }; + // eslint-disable-next-line unicorn/require-post-message-target-origin worker.postMessage(response); break; } diff --git a/packages/ws/src/strategies/sharding/worker.ts b/packages/ws/src/strategies/sharding/worker.ts index 8a4c0bce3cd0..be803e1aa227 100644 --- a/packages/ws/src/strategies/sharding/worker.ts +++ b/packages/ws/src/strategies/sharding/worker.ts @@ -1,14 +1,15 @@ +/* eslint-disable unicorn/require-post-message-target-origin */ import { isMainThread, workerData, parentPort } from 'node:worker_threads'; import { Collection } from '@discordjs/collection'; +import { WebSocketShard, WebSocketShardEvents, type WebSocketShardDestroyOptions } from '../../ws/WebSocketShard.js'; +import { WorkerContextFetchingStrategy } from '../context/WorkerContextFetchingStrategy.js'; import { - WorkerData, - WorkerRecievePayload, WorkerRecievePayloadOp, - WorkerSendPayload, WorkerSendPayloadOp, -} from './WorkerShardingStrategy'; -import { WebSocketShard, WebSocketShardDestroyOptions, WebSocketShardEvents } from '../../ws/WebSocketShard'; -import { WorkerContextFetchingStrategy } from '../context/WorkerContextFetchingStrategy'; + type WorkerData, + type WorkerRecievePayload, + type WorkerSendPayload, +} from './WorkerShardingStrategy.js'; if (isMainThread) { throw new Error('Expected worker script to not be ran within the main thread'); @@ -22,6 +23,7 @@ async function connect(shardId: number) { if (!shard) { throw new Error(`Shard ${shardId} does not exist`); } + await shard.connect(); } @@ -30,13 +32,14 @@ async function destroy(shardId: number, options?: WebSocketShardDestroyOptions) if (!shard) { throw new Error(`Shard ${shardId} does not exist`); } + await shard.destroy(options); } for (const shardId of data.shardIds) { const shard = new WebSocketShard(new WorkerContextFetchingStrategy(data), shardId); for (const event of Object.values(WebSocketShardEvents)) { - // @ts-expect-error + // @ts-expect-error: Event types incompatible shard.on(event, (data) => { const payload: WorkerRecievePayload = { op: WorkerRecievePayloadOp.Event, @@ -47,6 +50,7 @@ for (const shardId of data.shardIds) { parentPort!.postMessage(payload); }); } + shards.set(shardId, shard); } @@ -56,6 +60,7 @@ parentPort! }) // eslint-disable-next-line @typescript-eslint/no-misused-promises .on('message', async (payload: WorkerSendPayload) => { + // eslint-disable-next-line default-case switch (payload.op) { case WorkerSendPayloadOp.Connect: { await connect(payload.shardId); @@ -73,6 +78,7 @@ parentPort! op: WorkerRecievePayloadOp.Destroyed, shardId: payload.shardId, }; + parentPort!.postMessage(response); break; } @@ -82,6 +88,7 @@ parentPort! if (!shard) { throw new Error(`Shard ${payload.shardId} does not exist`); } + await shard.send(payload.payload); break; } diff --git a/packages/ws/src/utils/IdentifyThrottler.ts b/packages/ws/src/utils/IdentifyThrottler.ts index 22a51e9368b1..90faa3fac3aa 100644 --- a/packages/ws/src/utils/IdentifyThrottler.ts +++ b/packages/ws/src/utils/IdentifyThrottler.ts @@ -4,7 +4,7 @@ import type { WebSocketManager } from '../ws/WebSocketManager'; export class IdentifyThrottler { private identifyState = { remaining: 0, - resetsAt: Infinity, + resetsAt: Number.POSITIVE_INFINITY, }; public constructor(private readonly manager: WebSocketManager) {} diff --git a/packages/ws/src/utils/constants.ts b/packages/ws/src/utils/constants.ts index 1b9919a11fc6..8ccc9c6ed34e 100644 --- a/packages/ws/src/utils/constants.ts +++ b/packages/ws/src/utils/constants.ts @@ -1,9 +1,10 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; +import process from 'node:process'; import { Collection } from '@discordjs/collection'; import { APIVersion, GatewayOpcodes } from 'discord-api-types/v10'; -import { lazy } from './utils'; -import type { OptionalWebSocketManagerOptions, SessionInfo } from '../ws/WebSocketManager'; +import type { OptionalWebSocketManagerOptions, SessionInfo } from '../ws/WebSocketManager.js'; +import { lazy } from './utils.js'; /** * Valid encoding types diff --git a/packages/ws/src/utils/utils.ts b/packages/ws/src/utils/utils.ts index 5bb7196f96b5..858450decad3 100644 --- a/packages/ws/src/utils/utils.ts +++ b/packages/ws/src/utils/utils.ts @@ -1,6 +1,6 @@ import type { ShardRange } from '../ws/WebSocketManager'; -export type Awaitable = T | Promise; +export type Awaitable = Promise | T; /** * Yields the numbers in the given range as an array @@ -11,7 +11,7 @@ export type Awaitable = T | Promise; * ``` */ export function range({ start, end }: ShardRange): number[] { - return Array.from({ length: end - start + 1 }, (_, i) => i + start); + return Array.from({ length: end - start + 1 }, (_, index) => index + start); } /** diff --git a/packages/ws/src/ws/WebSocketManager.ts b/packages/ws/src/ws/WebSocketManager.ts index 9a7877a09d6f..fd6434dbb095 100644 --- a/packages/ws/src/ws/WebSocketManager.ts +++ b/packages/ws/src/ws/WebSocketManager.ts @@ -1,26 +1,26 @@ import type { REST } from '@discordjs/rest'; import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; import { - APIGatewayBotInfo, - GatewayIdentifyProperties, - GatewayPresenceUpdateData, - RESTGetAPIGatewayBotResult, - GatewayIntentBits, Routes, - GatewaySendPayload, + type APIGatewayBotInfo, + type GatewayIdentifyProperties, + type GatewayPresenceUpdateData, + type RESTGetAPIGatewayBotResult, + type GatewayIntentBits, + type GatewaySendPayload, } from 'discord-api-types/v10'; -import type { WebSocketShardDestroyOptions, WebSocketShardEventsMap } from './WebSocketShard'; import type { IShardingStrategy } from '../strategies/sharding/IShardingStrategy'; -import { SimpleShardingStrategy } from '../strategies/sharding/SimpleShardingStrategy'; -import { CompressionMethod, DefaultWebSocketManagerOptions, Encoding } from '../utils/constants'; -import { Awaitable, range } from '../utils/utils'; +import { SimpleShardingStrategy } from '../strategies/sharding/SimpleShardingStrategy.js'; +import { DefaultWebSocketManagerOptions, type CompressionMethod, type Encoding } from '../utils/constants.js'; +import { range, type Awaitable } from '../utils/utils.js'; +import type { WebSocketShardDestroyOptions, WebSocketShardEventsMap } from './WebSocketShard.js'; /** * Represents a range of shard ids */ export interface ShardRange { - start: number; end: number; + start: number; } /** @@ -28,35 +28,31 @@ export interface ShardRange { */ export interface SessionInfo { /** - * Session id for this shard + * URL to use when resuming */ - sessionId: string; + resumeURL: string; /** * The sequence number of the last message sent by the shard */ sequence: number; /** - * The id of the shard + * Session id for this shard */ - shardId: number; + sessionId: string; /** * The total number of shards at the time of this shard identifying */ shardCount: number; /** - * URL to use when resuming + * The id of the shard */ - resumeURL: string; + shardId: number; } /** * Required options for the WebSocketManager */ export interface RequiredWebSocketManagerOptions { - /** - * The token to use for identifying with the gateway - */ - token: string; /** * The intents to request */ @@ -65,6 +61,10 @@ export interface RequiredWebSocketManagerOptions { * The REST instance to use for fetching gateway information */ rest: REST; + /** + * The token to use for identifying with the gateway + */ + token: string; } /** @@ -72,59 +72,41 @@ export interface RequiredWebSocketManagerOptions { */ export interface OptionalWebSocketManagerOptions { /** - * The total number of shards across all WebsocketManagers you intend to instantiate. - * Use `null` to use Discord's recommended shard count + * The compression method to use + * + * @defaultValue `null` (no compression) */ - shardCount: number | null; + compression: CompressionMethod | null; /** - * The ids of the shards this WebSocketManager should manage. - * Use `null` to simply spawn 0 through `shardCount - 1` - * - * @example - * ```ts - * const manager = new WebSocketManager({ - * shardIds: [1, 3, 7], // spawns shard 1, 3, and 7, nothing else - * }); - * ``` + * The encoding to use * - * @example - * ```ts - * const manager = new WebSocketManager({ - * shardIds: { - * start: 3, - * end: 6, - * }, // spawns shards 3, 4, 5, and 6 - * }); - * ``` + * @defaultValue `'json'` */ - shardIds: number[] | ShardRange | null; + encoding: Encoding; /** - * Value between 50 and 250, total number of members where the gateway will stop sending offline members in the guild member list + * How long to wait for a shard to connect before giving up */ - largeThreshold: number | null; + handshakeTimeout: number | null; /** - * Initial presence data to send to the gateway when identifying + * How long to wait for a shard's HELLO packet before giving up */ - initialPresence: GatewayPresenceUpdateData | null; + helloTimeout: number | null; /** * Properties to send to the gateway when identifying */ identifyProperties: GatewayIdentifyProperties; /** - * The gateway version to use - * @defaultValue `'10'` + * Initial presence data to send to the gateway when identifying */ - version: string; + initialPresence: GatewayPresenceUpdateData | null; /** - * The encoding to use - * @defaultValue `'json'` + * Value between 50 and 250, total number of members where the gateway will stop sending offline members in the guild member list */ - encoding: Encoding; + largeThreshold: number | null; /** - * The compression method to use - * @defaultValue `null` (no compression) + * How long to wait for a shard's READY packet before giving up */ - compression: CompressionMethod | null; + readyTimeout: number | null; /** * Function used to retrieve session information (and attempt to resume) for a given shard * @@ -139,26 +121,46 @@ export interface OptionalWebSocketManagerOptions { * }); * ``` */ - retrieveSessionInfo: (shardId: number) => Awaitable; + retrieveSessionInfo(shardId: number): Awaitable; /** - * Function used to store session information for a given shard + * The total number of shards across all WebsocketManagers you intend to instantiate. + * Use `null` to use Discord's recommended shard count */ - updateSessionInfo: (shardId: number, sessionInfo: SessionInfo | null) => Awaitable; + shardCount: number | null; /** - * How long to wait for a shard to connect before giving up + * The ids of the shards this WebSocketManager should manage. + * Use `null` to simply spawn 0 through `shardCount - 1` + * + * @example + * ```ts + * const manager = new WebSocketManager({ + * shardIds: [1, 3, 7], // spawns shard 1, 3, and 7, nothing else + * }); + * ``` + * @example + * ```ts + * const manager = new WebSocketManager({ + * shardIds: { + * start: 3, + * end: 6, + * }, // spawns shards 3, 4, 5, and 6 + * }); + * ``` */ - handshakeTimeout: number | null; + shardIds: number[] | ShardRange | null; /** - * How long to wait for a shard's HELLO packet before giving up + * Function used to store session information for a given shard */ - helloTimeout: number | null; + updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable; /** - * How long to wait for a shard's READY packet before giving up + * The gateway version to use + * + * @defaultValue `'10'` */ - readyTimeout: number | null; + version: string; } -export type WebSocketManagerOptions = RequiredWebSocketManagerOptions & OptionalWebSocketManagerOptions; +export type WebSocketManagerOptions = OptionalWebSocketManagerOptions & RequiredWebSocketManagerOptions; export type ManagerShardEventsMap = { [K in keyof WebSocketShardEventsMap]: [ @@ -187,11 +189,12 @@ export class WebSocketManager extends AsyncEventEmitter { /** * Strategy used to manage shards + * * @defaultValue `SimpleManagerToShardStrategy` */ private strategy: IShardingStrategy = new SimpleShardingStrategy(this); - public constructor(options: RequiredWebSocketManagerOptions & Partial) { + public constructor(options: Partial & RequiredWebSocketManagerOptions) { super(); this.options = { ...DefaultWebSocketManagerOptions, ...options }; } @@ -203,6 +206,7 @@ export class WebSocketManager extends AsyncEventEmitter { /** * Fetches the gateway information from Discord - or returns it from cache if available + * * @param force - Whether to ignore the cache and force a fresh fetch */ public async fetchGatewayInformation(force = false) { @@ -222,6 +226,7 @@ export class WebSocketManager extends AsyncEventEmitter { /** * Updates your total shard count on-the-fly, spawning shards as needed + * * @param shardCount - The new shard count to use */ public async updateShardCount(shardCount: number | null) { diff --git a/packages/ws/src/ws/WebSocketShard.ts b/packages/ws/src/ws/WebSocketShard.ts index 88b6c8b2582d..e042f6b6a57b 100644 --- a/packages/ws/src/ws/WebSocketShard.ts +++ b/packages/ws/src/ws/WebSocketShard.ts @@ -1,6 +1,9 @@ +/* eslint-disable id-length */ +import { Buffer } from 'node:buffer'; import { once } from 'node:events'; -import { setTimeout } from 'node:timers'; +import { setTimeout, clearInterval, clearTimeout, setInterval } from 'node:timers'; import { setTimeout as sleep } from 'node:timers/promises'; +import { URLSearchParams } from 'node:url'; import { TextDecoder } from 'node:util'; import { inflate } from 'node:zlib'; import { Collection } from '@discordjs/collection'; @@ -9,27 +12,28 @@ import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; import { GatewayCloseCodes, GatewayDispatchEvents, - GatewayDispatchPayload, - GatewayIdentifyData, GatewayOpcodes, - GatewayReceivePayload, - GatewaySendPayload, + type GatewayDispatchPayload, + type GatewayIdentifyData, + type GatewayReceivePayload, + type GatewaySendPayload, } from 'discord-api-types/v10'; -import { RawData, WebSocket } from 'ws'; +import { WebSocket, type RawData } from 'ws'; import type { Inflate } from 'zlib-sync'; -import type { SessionInfo } from './WebSocketManager'; import type { IContextFetchingStrategy } from '../strategies/context/IContextFetchingStrategy'; -import { ImportantGatewayOpcodes } from '../utils/constants'; -import { lazy } from '../utils/utils'; +import { ImportantGatewayOpcodes } from '../utils/constants.js'; +import { lazy } from '../utils/utils.js'; +import type { SessionInfo } from './WebSocketManager.js'; -const getZlibSync = lazy(() => import('zlib-sync').then((mod) => mod.default).catch(() => null)); +// eslint-disable-next-line promise/prefer-await-to-then +const getZlibSync = lazy(async () => import('zlib-sync').then((mod) => mod.default).catch(() => null)); export enum WebSocketShardEvents { Debug = 'debug', + Dispatch = 'dispatch', Hello = 'hello', Ready = 'ready', Resumed = 'resumed', - Dispatch = 'dispatch', } export enum WebSocketShardStatus { @@ -54,14 +58,14 @@ export type WebSocketShardEventsMap = { }; export interface WebSocketShardDestroyOptions { - reason?: string; code?: number; + reason?: string; recover?: WebSocketShardDestroyRecovery; } export enum CloseCodes { - Normal = 1000, - Resuming = 4200, + Normal = 1_000, + Resuming = 4_200, } export class WebSocketShard extends AsyncEventEmitter { @@ -72,6 +76,7 @@ export class WebSocketShard extends AsyncEventEmitter { private useIdentifyCompress = false; private inflate: Inflate | null = null; + private readonly textDecoder = new TextDecoder(); private status: WebSocketShardStatus = WebSocketShardStatus.Idle; @@ -86,6 +91,7 @@ export class WebSocketShard extends AsyncEventEmitter { }; private heartbeatInterval: NodeJS.Timer | null = null; + private lastHeartbeatAt = -1; private session: SessionInfo | null = null; @@ -114,7 +120,7 @@ export class WebSocketShard extends AsyncEventEmitter { if (zlib) { params.append('compress', compression); this.inflate = new zlib.Inflate({ - chunkSize: 65535, + chunkSize: 65_535, to: 'string', }); } else if (!this.useIdentifyCompress) { @@ -173,6 +179,7 @@ export class WebSocketShard extends AsyncEventEmitter { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } + this.lastHeartbeatAt = -1; // Clear session state if applicable @@ -202,6 +209,7 @@ export class WebSocketShard extends AsyncEventEmitter { this.status = WebSocketShardStatus.Idle; if (options.recover !== undefined) { + // eslint-disable-next-line consistent-return return this.connect(); } } @@ -213,6 +221,7 @@ export class WebSocketShard extends AsyncEventEmitter { if (timeout) { this.timeouts.set(event, timeout); } + await once(this, event, { signal: controller.signal }); if (timeout) { clearTimeout(timeout); @@ -279,7 +288,7 @@ export class WebSocketShard extends AsyncEventEmitter { this.status = WebSocketShardStatus.Ready; } - private resume(session: SessionInfo) { + private async resume(session: SessionInfo) { this.debug(['Resuming session']); this.status = WebSocketShardStatus.Resuming; this.replayedEvents = 0; @@ -293,6 +302,7 @@ export class WebSocketShard extends AsyncEventEmitter { }); } + // eslint-disable-next-line consistent-return private async heartbeat(requested = false) { if (!this.isAck && !requested) { return this.destroy({ reason: 'Zombie connection', recover: WebSocketShardDestroyRecovery.Resume }); @@ -307,7 +317,7 @@ export class WebSocketShard extends AsyncEventEmitter { this.isAck = false; } - private async unpackMessage(data: Buffer | ArrayBuffer, isBinary: boolean): Promise { + private async unpackMessage(data: ArrayBuffer | Buffer, isBinary: boolean): Promise { const decompressable = new Uint8Array(data); // Deal with no compression @@ -318,9 +328,10 @@ export class WebSocketShard extends AsyncEventEmitter { // Deal with identify compress if (this.useIdentifyCompress) { return new Promise((resolve, reject) => { - inflate(decompressable, { chunkSize: 65535 }, (err, result) => { + inflate(decompressable, { chunkSize: 65_535 }, (err, result) => { if (err) { - return reject(err); + reject(err); + return; } resolve(JSON.parse(this.textDecoder.decode(result)) as GatewayReceivePayload); @@ -368,11 +379,12 @@ export class WebSocketShard extends AsyncEventEmitter { } private async onMessage(data: RawData, isBinary: boolean) { - const payload = await this.unpackMessage(data as Buffer | ArrayBuffer, isBinary); + const payload = await this.unpackMessage(data as ArrayBuffer | Buffer, isBinary); if (!payload) { return; } + // eslint-disable-next-line default-case switch (payload.op) { case GatewayOpcodes.Dispatch: { if (this.status === WebSocketShardStatus.Ready || this.status === WebSocketShardStatus.Resuming) { @@ -411,11 +423,9 @@ export class WebSocketShard extends AsyncEventEmitter { } } - if (this.session) { - if (payload.s > this.session.sequence) { - this.session.sequence = payload.s; - await this.strategy.updateSessionInfo(this.id, this.session); - } + if (this.session && payload.s > this.session.sequence) { + this.session.sequence = payload.s; + await this.strategy.updateSessionInfo(this.id, this.session); } break; @@ -447,6 +457,7 @@ export class WebSocketShard extends AsyncEventEmitter { recover: WebSocketShardDestroyRecovery.Reconnect, }); } + break; } @@ -469,6 +480,7 @@ export class WebSocketShard extends AsyncEventEmitter { this.emit('error', err); } + // eslint-disable-next-line consistent-return private async onClose(code: number) { switch (code) { case CloseCodes.Normal: { diff --git a/yarn.lock b/yarn.lock index 749707f67125..d82d20ebb2e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,67 @@ __metadata: languageName: node linkType: hard +"@angular-eslint/bundled-angular-compiler@npm:14.0.3": + version: 14.0.3 + resolution: "@angular-eslint/bundled-angular-compiler@npm:14.0.3" + checksum: 7f686f52d34d20767d9ab5fcc70c0a228b23c4a17b79b774c8568293cdbcfdad6492c432b1462ecb974bd948f4f52cf59db9ba5ae5bd703442864a33adbf89a3 + languageName: node + linkType: hard + +"@angular-eslint/eslint-plugin-template@npm:^14.0.3": + version: 14.0.3 + resolution: "@angular-eslint/eslint-plugin-template@npm:14.0.3" + dependencies: + "@angular-eslint/bundled-angular-compiler": 14.0.3 + "@typescript-eslint/utils": 5.29.0 + aria-query: 5.0.0 + axobject-query: 3.0.1 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: "*" + checksum: 16b20170f2c69fb59f67cc18b26f451a92c18aaaac1cf64e544f6cb9bdf2083815674c2086c30e78e1c97b00fc5b52f11f9d715ac3d151cc982fc74c54809e98 + languageName: node + linkType: hard + +"@angular-eslint/eslint-plugin@npm:^14.0.3": + version: 14.0.3 + resolution: "@angular-eslint/eslint-plugin@npm:14.0.3" + dependencies: + "@angular-eslint/utils": 14.0.3 + "@typescript-eslint/utils": 5.29.0 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: "*" + checksum: 9c4296c61b1e2cc27ff72facfc87f68fdbc64e25125a684b7d6d804267526d6ab127baf00044238f0cbedb466ecda46d60fdfd1bb9740dbf7140d514353f7095 + languageName: node + linkType: hard + +"@angular-eslint/template-parser@npm:^14.0.3": + version: 14.0.3 + resolution: "@angular-eslint/template-parser@npm:14.0.3" + dependencies: + "@angular-eslint/bundled-angular-compiler": 14.0.3 + eslint-scope: ^5.1.0 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: "*" + checksum: 921bdd9336bb95773b5c7eb63c5759a6ab55e1f3f59458b0e37609d4e73b54eaa6a8679e8c149f7e4ec1bccfbb00f29605cb85e6f2c4ba83c54a4e3245cfb0fd + languageName: node + linkType: hard + +"@angular-eslint/utils@npm:14.0.3": + version: 14.0.3 + resolution: "@angular-eslint/utils@npm:14.0.3" + dependencies: + "@angular-eslint/bundled-angular-compiler": 14.0.3 + "@typescript-eslint/utils": 5.29.0 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: "*" + checksum: 937a66534858ad10609781e9e4eaf194947d0f212f128223a896a17a624a0f26684e74a7e873d5f692d4a5bb77ea3d45272d99715d6c292407ceb017ac9cc4cf + languageName: node + linkType: hard + "@antfu/install-pkg@npm:^0.1.0": version: 0.1.0 resolution: "@antfu/install-pkg@npm:0.1.0" @@ -1358,7 +1419,17 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.8.4": +"@babel/runtime-corejs3@npm:^7.10.2": + version: 7.18.9 + resolution: "@babel/runtime-corejs3@npm:7.18.9" + dependencies: + core-js-pure: ^3.20.2 + regenerator-runtime: ^0.13.4 + checksum: 249158b660ac996fa4f4b0d1ab5810db060af40fac4d0bb5da23f55539a151313ae254aa64afc2ab7000d95167824e21a689f74bc24b36fd0f5ca030d522133d + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.8.4": version: 7.18.9 resolution: "@babel/runtime@npm:7.18.9" dependencies: @@ -1668,14 +1739,9 @@ __metadata: dependencies: "@actions/core": ^1.9.1 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@vitest/coverage-c8": ^0.22.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 + eslint-config-neon: ^0.1.23 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 tslib: ^2.4.0 @@ -1692,14 +1758,8 @@ __metadata: "@microsoft/api-extractor-model": ^7.23.3 "@microsoft/tsdoc": ^0.14.1 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-tsdoc: ^0.2.16 + eslint-config-neon: ^0.1.23 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 typescript: ^4.8.2 @@ -1716,17 +1776,11 @@ __metadata: "@microsoft/api-extractor": ^7.29.5 "@sapphire/shapeshift": ^3.6.0 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@vitest/coverage-c8": ^0.22.1 discord-api-types: ^0.37.5 downlevel-dts: ^0.10.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-tsdoc: ^0.2.16 + eslint-config-neon: ^0.1.23 fast-deep-equal: ^3.1.3 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 @@ -1746,16 +1800,10 @@ __metadata: "@favware/cliff-jumper": ^1.8.7 "@microsoft/api-extractor": ^7.29.5 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@vitest/coverage-c8": ^0.22.1 downlevel-dts: ^0.10.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-tsdoc: ^0.2.16 + eslint-config-neon: ^0.1.23 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 typescript: ^4.8.2 @@ -1790,14 +1838,9 @@ __metadata: "@favware/cliff-jumper": ^1.8.7 "@types/jsdoc-to-markdown": ^7.0.3 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 commander: ^9.4.0 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 + eslint-config-neon: ^0.1.23 jsdoc-to-markdown: ^7.1.1 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 @@ -1817,13 +1860,8 @@ __metadata: "@discordjs/proxy": ^1.1.0 "@discordjs/rest": ^1.1.0 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 + eslint-config-neon: ^0.1.23 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 tslib: ^2.4.0 @@ -1842,15 +1880,10 @@ __metadata: "@microsoft/api-extractor": ^7.29.5 "@types/node": ^16.11.56 "@types/supertest": ^2.0.12 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@vitest/coverage-c8": ^0.22.1 downlevel-dts: ^0.10.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 + eslint-config-neon: ^0.1.23 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 supertest: ^6.2.4 @@ -1873,17 +1906,11 @@ __metadata: "@sapphire/async-queue": ^1.5.0 "@sapphire/snowflake": ^3.2.2 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@vitest/coverage-c8": ^0.22.1 discord-api-types: ^0.37.5 downlevel-dts: ^0.10.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-tsdoc: ^0.2.16 + eslint-config-neon: ^0.1.23 file-type: ^18.0.0 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 @@ -1903,15 +1930,10 @@ __metadata: "@microsoft/api-extractor-model": ^7.23.3 "@microsoft/tsdoc": ^0.14.1 "@types/node": ^16.11.56 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@vitest/coverage-c8": ^0.22.1 commander: ^9.4.0 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 + eslint-config-neon: ^0.1.23 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 tslib: ^2.4.0 @@ -1934,16 +1956,10 @@ __metadata: "@types/jest": ^28.1.8 "@types/node": ^16.11.56 "@types/ws": ^8.5.3 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 discord-api-types: ^0.37.5 downlevel-dts: ^0.10.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-tsdoc: ^0.2.16 + eslint-config-neon: ^0.1.23 jest: ^29.0.1 jest-websocket-mock: ^2.4.0 mock-socket: ^9.1.5 @@ -1978,8 +1994,6 @@ __metadata: "@types/node": ^16.11.56 "@types/react-dom": ^18.0.6 "@types/react-syntax-highlighter": ^15.5.5 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@unocss/cli": ^0.45.13 "@unocss/preset-web-fonts": ^0.45.13 "@unocss/reset": ^0.45.13 @@ -1988,12 +2002,7 @@ __metadata: "@vscode/codicons": ^0.0.32 concurrently: ^7.3.0 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-react: ^7.31.1 - eslint-plugin-react-hooks: ^4.6.0 + eslint-config-neon: ^0.1.23 happy-dom: ^6.0.4 next: ^12.2.5 next-mdx-remote: ^4.1.0 @@ -2029,18 +2038,12 @@ __metadata: "@sapphire/async-queue": ^1.5.0 "@types/node": ^16.11.56 "@types/ws": ^8.5.3 - "@typescript-eslint/eslint-plugin": ^5.36.1 - "@typescript-eslint/parser": ^5.36.1 "@vitest/coverage-c8": ^0.22.1 "@vladfrangu/async_event_emitter": ^2.0.1 discord-api-types: ^0.37.5 downlevel-dts: ^0.10.1 eslint: ^8.23.0 - eslint-config-marine: ^9.4.1 - eslint-config-prettier: ^8.5.0 - eslint-import-resolver-typescript: ^3.5.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-tsdoc: ^0.2.16 + eslint-config-neon: ^0.1.23 mock-socket: ^9.1.5 prettier: ^2.7.1 rollup-plugin-typescript2: ^0.33.0 @@ -2216,6 +2219,17 @@ __metadata: languageName: node linkType: hard +"@es-joy/jsdoccomment@npm:~0.31.0": + version: 0.31.0 + resolution: "@es-joy/jsdoccomment@npm:0.31.0" + dependencies: + comment-parser: 1.3.1 + esquery: ^1.4.0 + jsdoc-type-pratt-parser: ~3.1.0 + checksum: 1691ff501559f45593e5f080d2c08dea4fadba5f48e526b9ff2943c050fbb40408f5e83968542e5b6bf47219c7573796d00bfe80dacfd1ba8187904cc475cefb + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.14.54": version: 0.14.54 resolution: "@esbuild/linux-loong64@npm:0.14.54" @@ -2974,6 +2988,15 @@ __metadata: languageName: node linkType: hard +"@next/eslint-plugin-next@npm:^12.2.5": + version: 12.2.5 + resolution: "@next/eslint-plugin-next@npm:12.2.5" + dependencies: + glob: 7.1.7 + checksum: 0d6faf895d4952fc2a5da3f2e86a9e1903f37b44201e7fabfcc994f6989dfceb974f354a7abb1779b318f14ada57925d82eb6c22628265c7f6b36f232edc93ee + languageName: node + linkType: hard + "@next/swc-android-arm-eabi@npm:12.2.5": version: 12.2.5 resolution: "@next/swc-android-arm-eabi@npm:12.2.5" @@ -3112,20 +3135,6 @@ __metadata: languageName: node linkType: hard -"@pkgr/utils@npm:^2.3.0": - version: 2.3.0 - resolution: "@pkgr/utils@npm:2.3.0" - dependencies: - cross-spawn: ^7.0.3 - is-glob: ^4.0.3 - open: ^8.4.0 - picocolors: ^1.0.0 - tiny-glob: ^0.2.9 - tslib: ^2.4.0 - checksum: d2fe95c51d548497425182b284dea8155d9e80bb36fa744c670174d7deae53743b13aa9f63a8bfaa8b31dcf7c53dc279a99372f2c2c35ff9fcebf91c5c5c8190 - languageName: node - linkType: hard - "@polka/url@npm:^1.0.0-next.20": version: 1.0.0-next.21 resolution: "@polka/url@npm:1.0.0-next.21" @@ -3379,6 +3388,13 @@ __metadata: languageName: node linkType: hard +"@rushstack/eslint-patch@npm:^1.1.4": + version: 1.1.4 + resolution: "@rushstack/eslint-patch@npm:1.1.4" + checksum: 597bc84e2f76c7f5f2bcedd4c4b1dd5d02524167a0f67ac588e8fbbd94666297aaf0e6a53ec46afb95554164fc1169ff782841003280e4bc98e80ab6559412c6 + languageName: node + linkType: hard + "@rushstack/node-core-library@npm:3.51.1": version: 3.51.1 resolution: "@rushstack/node-core-library@npm:3.51.1" @@ -3844,13 +3860,6 @@ __metadata: languageName: node linkType: hard -"@types/json5@npm:^0.0.29": - version: 0.0.29 - resolution: "@types/json5@npm:0.0.29" - checksum: e60b153664572116dfea673c5bda7778dbff150498f44f998e34b5886d8afc47f16799280e4b6e241c0472aef1bc36add771c569c68fc5125fc2ae519a3eb9ac - languageName: node - linkType: hard - "@types/keyv@npm:^3.1.1": version: 3.1.4 resolution: "@types/keyv@npm:3.1.4" @@ -4140,6 +4149,17 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/experimental-utils@npm:^5.0.0": + version: 5.36.1 + resolution: "@typescript-eslint/experimental-utils@npm:5.36.1" + dependencies: + "@typescript-eslint/utils": 5.36.1 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 58bd9df609e0323c1c0bfd8e65956defe3e775629d58d39d459d9ee860ebded5ecf55a26ddfe90a7420a3f11537ef95a93bbcc49110d36511a177a7ad179bdaf + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:^5.36.1": version: 5.36.1 resolution: "@typescript-eslint/parser@npm:5.36.1" @@ -4157,6 +4177,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:5.29.0": + version: 5.29.0 + resolution: "@typescript-eslint/scope-manager@npm:5.29.0" + dependencies: + "@typescript-eslint/types": 5.29.0 + "@typescript-eslint/visitor-keys": 5.29.0 + checksum: 540642bef9c55fd7692af98dfb42ab99eeb82553f42ab2a3c4e132e02b5ba492da1c6bf1ca6b02b900a678fc74399ad6c564c0ca20d91032090b6cbcb01a5bde + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:5.36.1": version: 5.36.1 resolution: "@typescript-eslint/scope-manager@npm:5.36.1" @@ -4184,6 +4214,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.29.0": + version: 5.29.0 + resolution: "@typescript-eslint/types@npm:5.29.0" + checksum: 982ecdd69103105cabff8deac7f82f6002cf909238702ce902133e9af655cd962f977d5adf650c5ddae80d8c0e46abe1612a9141b25c7ed20ba8d662eb7ab871 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:5.36.1": version: 5.36.1 resolution: "@typescript-eslint/types@npm:5.36.1" @@ -4191,6 +4228,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:5.29.0": + version: 5.29.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.29.0" + dependencies: + "@typescript-eslint/types": 5.29.0 + "@typescript-eslint/visitor-keys": 5.29.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + semver: ^7.3.7 + tsutils: ^3.21.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: b91107a9fc31bf511aaa70f1e6d1d832d3acf08cfe999c870169447a7c223abff54c1d604bbb08d7c069dd98b43fb279bc314d1726097704fe8ad4c6e0969b12 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.36.1": version: 5.36.1 resolution: "@typescript-eslint/typescript-estree@npm:5.36.1" @@ -4209,7 +4264,23 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.36.1": +"@typescript-eslint/utils@npm:5.29.0": + version: 5.29.0 + resolution: "@typescript-eslint/utils@npm:5.29.0" + dependencies: + "@types/json-schema": ^7.0.9 + "@typescript-eslint/scope-manager": 5.29.0 + "@typescript-eslint/types": 5.29.0 + "@typescript-eslint/typescript-estree": 5.29.0 + eslint-scope: ^5.1.1 + eslint-utils: ^3.0.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 216f51fb9c176437919af55db9ed14db8af7b020611e954c06e69956b9e3248cbfe6a218013d6c17b716116dca6566a4c03710f9b48ce4e94f89312d61c16d07 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:5.36.1, @typescript-eslint/utils@npm:^5.10.0": version: 5.36.1 resolution: "@typescript-eslint/utils@npm:5.36.1" dependencies: @@ -4225,6 +4296,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:5.29.0": + version: 5.29.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.29.0" + dependencies: + "@typescript-eslint/types": 5.29.0 + eslint-visitor-keys: ^3.3.0 + checksum: 15f228ad9ffaf0e42cc6b2ee4e5095c1e89c4c2dc46a65d19ca0e2296d88c08a1192039d942bd9600b1496176749f6322d636dd307602dbab90404a9501b4d6e + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.36.1": version: 5.36.1 resolution: "@typescript-eslint/visitor-keys@npm:5.36.1" @@ -4911,13 +4992,23 @@ __metadata: languageName: node linkType: hard -"aria-query@npm:^5.0.0": +"aria-query@npm:5.0.0, aria-query@npm:^5.0.0": version: 5.0.0 resolution: "aria-query@npm:5.0.0" checksum: c41f98866c5a304561ee8cae55856711cddad6f3f85d8cb43cc5f79667078d9b8979ce32d244c1ff364e6463a4d0b6865804a33ccc717fed701b281cf7dc6296 languageName: node linkType: hard +"aria-query@npm:^4.2.2": + version: 4.2.2 + resolution: "aria-query@npm:4.2.2" + dependencies: + "@babel/runtime": ^7.10.2 + "@babel/runtime-corejs3": ^7.10.2 + checksum: 38401a9a400f26f3dcc24b84997461a16b32869a9893d323602bed8da40a8bcc0243b8d2880e942249a1496cea7a7de769e93d21c0baa439f01e1ee936fed665 + languageName: node + linkType: hard + "array-back@npm:^1.0.2, array-back@npm:^1.0.3": version: 1.0.4 resolution: "array-back@npm:1.0.4" @@ -4971,7 +5062,7 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.4, array-includes@npm:^3.1.5": +"array-includes@npm:^3.1.5": version: 3.1.5 resolution: "array-includes@npm:3.1.5" dependencies: @@ -4991,7 +5082,7 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.2.5": +"array.prototype.flat@npm:^1.3.0": version: 1.3.0 resolution: "array.prototype.flat@npm:1.3.0" dependencies: @@ -5015,6 +5106,19 @@ __metadata: languageName: node linkType: hard +"array.prototype.reduce@npm:^1.0.4": + version: 1.0.4 + resolution: "array.prototype.reduce@npm:1.0.4" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.2 + es-array-method-boxes-properly: ^1.0.0 + is-string: ^1.0.7 + checksum: 6a57a1a2d3b77a9543db139cd52211f43a5af8e8271cb3c173be802076e3a6f71204ba8f090f5937ebc0842d5876db282f0f63dffd0e86b153e6e5a45681e4a5 + languageName: node + linkType: hard + "arrify@npm:^1.0.1": version: 1.0.1 resolution: "arrify@npm:1.0.1" @@ -5052,6 +5156,13 @@ __metadata: languageName: node linkType: hard +"ast-types-flow@npm:^0.0.7": + version: 0.0.7 + resolution: "ast-types-flow@npm:0.0.7" + checksum: a26dcc2182ffee111cad7c471759b0bda22d3b7ebacf27c348b22c55f16896b18ab0a4d03b85b4020dce7f3e634b8f00b593888f622915096ea1927fa51866c4 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -5096,6 +5207,27 @@ __metadata: languageName: node linkType: hard +"axe-core@npm:^4.4.3": + version: 4.4.3 + resolution: "axe-core@npm:4.4.3" + checksum: c3ea000d9ace3ba0bc747c8feafc24b0de62a0f7d93021d0f77b19c73fca15341843510f6170da563d51535d6cfb7a46c5fc0ea36170549dbb44b170208450a2 + languageName: node + linkType: hard + +"axobject-query@npm:3.0.1": + version: 3.0.1 + resolution: "axobject-query@npm:3.0.1" + checksum: c4eede240d332936c85b15725695f55797304366063ee025db5a0c5df98bb35ec826129191c3e91a0355a56854029c7bd6f63b505022737b6e3ab83cef6bf7df + languageName: node + linkType: hard + +"axobject-query@npm:^2.2.0": + version: 2.2.0 + resolution: "axobject-query@npm:2.2.0" + checksum: 96b8c7d807ca525f41ad9b286186e2089b561ba63a6d36c3e7d73dc08150714660995c7ad19cda05784458446a0793b45246db45894631e13853f48c1aa3117f + languageName: node + linkType: hard + "babel-code-frame@npm:^6.22.0": version: 6.26.0 resolution: "babel-code-frame@npm:6.26.0" @@ -5429,7 +5561,7 @@ __metadata: languageName: node linkType: hard -"builtins@npm:^5.0.0": +"builtins@npm:^5.0.0, builtins@npm:^5.0.1": version: 5.0.1 resolution: "builtins@npm:5.0.1" dependencies: @@ -5759,7 +5891,7 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.1.0, ci-info@npm:^3.2.0": +"ci-info@npm:^3.1.0, ci-info@npm:^3.2.0, ci-info@npm:^3.3.2": version: 3.3.2 resolution: "ci-info@npm:3.3.2" checksum: fd81f1edd2d3b0f6cb077b2e84365136d87b9db8c055928c1ad69da8a76c2c2f19cba8ea51b90238302157ca927f91f92b653e933f2398dde4867500f08d6e62 @@ -5773,6 +5905,15 @@ __metadata: languageName: node linkType: hard +"clean-regexp@npm:^1.0.0": + version: 1.0.0 + resolution: "clean-regexp@npm:1.0.0" + dependencies: + escape-string-regexp: ^1.0.5 + checksum: 0b1ce281b07da2463c6882ea2e8409119b6cabbd9f687cdbdcee942c45b2b9049a2084f7b5f228c63ef9f21e722963ae0bfe56a735dbdbdd92512867625a7e40 + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -6048,6 +6189,13 @@ __metadata: languageName: node linkType: hard +"comment-parser@npm:1.3.1": + version: 1.3.1 + resolution: "comment-parser@npm:1.3.1" + checksum: 421e6a113a3afd548500e7174ab46a2049dccf92e82bbaa3b209031b1bdf97552aabfa1ae2a120c0b62df17e1ba70e0d8b05d68504fee78e1ef974c59bcfe718 + languageName: node + linkType: hard + "common-sequence@npm:^2.0.2": version: 2.0.2 resolution: "common-sequence@npm:2.0.2" @@ -6410,6 +6558,13 @@ __metadata: languageName: node linkType: hard +"core-js-pure@npm:^3.20.2": + version: 3.25.0 + resolution: "core-js-pure@npm:3.25.0" + checksum: 041cef3c4fa03b30eea6aa8539db00a02ea264e8542b9b787428f43e727e67050c742f46dbd75bc9ab544524a54e1ee55d8b23602dc8a2da485a3741a5f95df7 + languageName: node + linkType: hard + "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -6521,6 +6676,13 @@ __metadata: languageName: node linkType: hard +"damerau-levenshtein@npm:^1.0.8": + version: 1.0.8 + resolution: "damerau-levenshtein@npm:1.0.8" + checksum: d240b7757544460ae0586a341a53110ab0a61126570ef2d8c731e3eab3f0cb6e488e2609e6a69b46727635de49be20b071688698744417ff1b6c1d7ccd03e0de + languageName: node + linkType: hard + "dargs@npm:^7.0.0": version: 7.0.0 resolution: "dargs@npm:7.0.0" @@ -6669,13 +6831,6 @@ __metadata: languageName: node linkType: hard -"define-lazy-prop@npm:^2.0.0": - version: 2.0.0 - resolution: "define-lazy-prop@npm:2.0.0" - checksum: 0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 - languageName: node - linkType: hard - "define-properties@npm:^1.1.3, define-properties@npm:^1.1.4": version: 1.1.4 resolution: "define-properties@npm:1.1.4" @@ -7088,16 +7243,6 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.10.0": - version: 5.10.0 - resolution: "enhanced-resolve@npm:5.10.0" - dependencies: - graceful-fs: ^4.2.4 - tapable: ^2.2.0 - checksum: 0bb9830704db271610f900e8d79d70a740ea16f251263362b0c91af545576d09fe50103496606c1300a05e588372d6f9780a9bc2e30ce8ef9b827ec8f44687ff - languageName: node - linkType: hard - "entities@npm:^2.0.0": version: 2.2.0 resolution: "entities@npm:2.2.0" @@ -7142,7 +7287,7 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5": +"es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.1": version: 1.20.1 resolution: "es-abstract@npm:1.20.1" dependencies: @@ -7173,6 +7318,13 @@ __metadata: languageName: node linkType: hard +"es-array-method-boxes-properly@npm:^1.0.0": + version: 1.0.0 + resolution: "es-array-method-boxes-properly@npm:1.0.0" + checksum: 2537fcd1cecf187083890bc6f5236d3a26bf39237433587e5bf63392e88faae929dbba78ff0120681a3f6f81c23fe3816122982c160d63b38c95c830b633b826 + languageName: node + linkType: hard + "es-module-lexer@npm:^0.9.3": version: 0.9.3 resolution: "es-module-lexer@npm:0.9.3" @@ -7881,19 +8033,32 @@ __metadata: languageName: node linkType: hard -"eslint-config-aqua@npm:^9.2.0": - version: 9.2.1 - resolution: "eslint-config-aqua@npm:9.2.1" - checksum: f91424cdcc227d29dd299c69bb030dcaa8fb7c03feced29297de24cf04b382d5cf81f0822ac2050321ca6ddbb0345f8f34534315821ecf315e76935ce888238a - languageName: node - linkType: hard - -"eslint-config-marine@npm:^9.4.1": - version: 9.4.1 - resolution: "eslint-config-marine@npm:9.4.1" +"eslint-config-neon@npm:^0.1.23": + version: 0.1.23 + resolution: "eslint-config-neon@npm:0.1.23" dependencies: - eslint-config-aqua: ^9.2.0 - checksum: 380969b39b9a5c43fcb8cfc283cee83ef046084e854c59e53dfc6eb08578e92914be12b992eb3cd6231610c008657ac4c03ecd96f8983efaa146c41f0f07cc09 + "@angular-eslint/eslint-plugin": ^14.0.3 + "@angular-eslint/eslint-plugin-template": ^14.0.3 + "@angular-eslint/template-parser": ^14.0.3 + "@next/eslint-plugin-next": ^12.2.5 + "@rushstack/eslint-patch": ^1.1.4 + "@typescript-eslint/eslint-plugin": ^5.36.1 + "@typescript-eslint/parser": ^5.36.1 + eslint-config-prettier: ^8.5.0 + eslint-plugin-cypress: ^2.12.1 + eslint-plugin-import: "npm:eslint-plugin-i@latest" + eslint-plugin-jest: ^27.0.1 + eslint-plugin-jsdoc: ^39.3.6 + eslint-plugin-jsx-a11y: ^6.6.1 + eslint-plugin-lodash: ^7.4.0 + eslint-plugin-n: ^15.2.5 + eslint-plugin-promise: ^6.0.1 + eslint-plugin-react: ^7.31.1 + eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-tsdoc: ^0.2.16 + eslint-plugin-typescript-sort-keys: ^2.1.0 + eslint-plugin-unicorn: ^43.0.2 + checksum: 68197bdec01e2fd44e14c62af5ee631c4453a491ff0f6f85ec591b0bc08a5eacb32016ee8e1188d10d4468bc0b92e8f103be74be9128a2ba4a7249578739b3cc languageName: node linkType: hard @@ -7934,24 +8099,6 @@ __metadata: languageName: node linkType: hard -"eslint-import-resolver-typescript@npm:^3.5.0": - version: 3.5.0 - resolution: "eslint-import-resolver-typescript@npm:3.5.0" - dependencies: - debug: ^4.3.4 - enhanced-resolve: ^5.10.0 - get-tsconfig: ^4.2.0 - globby: ^13.1.2 - is-core-module: ^2.10.0 - is-glob: ^4.0.3 - synckit: ^0.8.3 - peerDependencies: - eslint: "*" - eslint-plugin-import: "*" - checksum: 9719d1f68b7bb0eaf8939cff2d3b02b526949f73db744877de781640650dd4d0a17d934222b9ac69e27d9f363ee4569c1aa1a2a2aab6500257517f9bf7d25976 - languageName: node - linkType: hard - "eslint-module-utils@npm:^2.7.3": version: 2.7.4 resolution: "eslint-module-utils@npm:2.7.4" @@ -7964,26 +8111,146 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-import@npm:^2.26.0": - version: 2.26.0 - resolution: "eslint-plugin-import@npm:2.26.0" +"eslint-plugin-cypress@npm:^2.12.1": + version: 2.12.1 + resolution: "eslint-plugin-cypress@npm:2.12.1" + dependencies: + globals: ^11.12.0 + peerDependencies: + eslint: ">= 3.2.1" + checksum: 1f1c36e149304e9a6f58e2292a761abad58274da33b3a48b24ad55ad20cbce3ac7467321f2b6fcb052f9563c89f67004de4766eba2e2bdbcb010a6a0666989cf + languageName: node + linkType: hard + +"eslint-plugin-es@npm:^4.1.0": + version: 4.1.0 + resolution: "eslint-plugin-es@npm:4.1.0" + dependencies: + eslint-utils: ^2.0.0 + regexpp: ^3.0.0 + peerDependencies: + eslint: ">=4.19.1" + checksum: 26b87a216d3625612b1d3ca8653ac8a1d261046d2a973bb0eb2759070267d2bfb0509051facdeb5ae03dc8dfb51a434be23aff7309a752ca901d637da535677f + languageName: node + linkType: hard + +"eslint-plugin-import@npm:eslint-plugin-i@latest": + version: 2.26.0-2 + resolution: "eslint-plugin-i@npm:2.26.0-2" dependencies: - array-includes: ^3.1.4 - array.prototype.flat: ^1.2.5 + array-includes: ^3.1.5 + array.prototype.flat: ^1.3.0 debug: ^2.6.9 doctrine: ^2.1.0 eslint-import-resolver-node: ^0.3.6 eslint-module-utils: ^2.7.3 + get-own-property-symbols: ^0.9.5 + get-tsconfig: ^4.2.0 has: ^1.0.3 - is-core-module: ^2.8.1 + is-core-module: ^2.9.0 is-glob: ^4.0.3 minimatch: ^3.1.2 + object.getownpropertydescriptors: ^2.1.4 object.values: ^1.1.5 - resolve: ^1.22.0 - tsconfig-paths: ^3.14.1 + resolve: ^1.22.1 peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 0bf77ad80339554481eafa2b1967449e1f816b94c7a6f9614ce33fb4083c4e6c050f10d241dd50b4975d47922880a34de1e42ea9d8e6fd663ebb768baa67e655 + checksum: ec3f4694d6fe7138592ccbe3007ea35473a0162f0f2f1012606653dd173f0aca7061aad96560a261b825b49e55cae927ed30a2c8d47a56211d02901406e8c46d + languageName: node + linkType: hard + +"eslint-plugin-jest@npm:^27.0.1": + version: 27.0.1 + resolution: "eslint-plugin-jest@npm:27.0.1" + dependencies: + "@typescript-eslint/utils": ^5.10.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^5.0.0 + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: 269d4dc46bb925eb4c19106fd9e03775a863f53e05716628cc47777abc15887775ee47d73c4f8bdd98bb26b7462d8d8f654610bb2a367f8c97881204a2c3f42e + languageName: node + linkType: hard + +"eslint-plugin-jsdoc@npm:^39.3.6": + version: 39.3.6 + resolution: "eslint-plugin-jsdoc@npm:39.3.6" + dependencies: + "@es-joy/jsdoccomment": ~0.31.0 + comment-parser: 1.3.1 + debug: ^4.3.4 + escape-string-regexp: ^4.0.0 + esquery: ^1.4.0 + semver: ^7.3.7 + spdx-expression-parse: ^3.0.1 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 0825a5eba6cdcb250e45cd5ad488bd234da346f324a11160ad4b8c9fb3c76d8e1457d462fa91c24f11bdff5ef0013375d65c366b648202254c4bcc79eed89060 + languageName: node + linkType: hard + +"eslint-plugin-jsx-a11y@npm:^6.6.1": + version: 6.6.1 + resolution: "eslint-plugin-jsx-a11y@npm:6.6.1" + dependencies: + "@babel/runtime": ^7.18.9 + aria-query: ^4.2.2 + array-includes: ^3.1.5 + ast-types-flow: ^0.0.7 + axe-core: ^4.4.3 + axobject-query: ^2.2.0 + damerau-levenshtein: ^1.0.8 + emoji-regex: ^9.2.2 + has: ^1.0.3 + jsx-ast-utils: ^3.3.2 + language-tags: ^1.0.5 + minimatch: ^3.1.2 + semver: ^6.3.0 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + checksum: baae7377f0e25a0cc9b34dc333a3dc6ead9ee8365e445451eff554c3ca267a0a6cb88127fe90395c578ab1b92cfed246aef7dc8d2b48b603389e10181799e144 + languageName: node + linkType: hard + +"eslint-plugin-lodash@npm:^7.4.0": + version: 7.4.0 + resolution: "eslint-plugin-lodash@npm:7.4.0" + dependencies: + lodash: ^4.17.21 + peerDependencies: + eslint: ">=2" + checksum: 7557cded64dd0e1042b420214e65ba9d6c5cb6c83c40e471db1f7d33e63584d1260c9ca9a4fded4ca7a2fe2ac2a9cdc303e072105096fa99b583101c6e7ada13 + languageName: node + linkType: hard + +"eslint-plugin-n@npm:^15.2.5": + version: 15.2.5 + resolution: "eslint-plugin-n@npm:15.2.5" + dependencies: + builtins: ^5.0.1 + eslint-plugin-es: ^4.1.0 + eslint-utils: ^3.0.0 + ignore: ^5.1.1 + is-core-module: ^2.10.0 + minimatch: ^3.1.2 + resolve: ^1.22.1 + semver: ^7.3.7 + peerDependencies: + eslint: ">=7.0.0" + checksum: 3be265957b3dda6a049841803335c17689cf98a4b3859eeed3e57b44850b241e7d20640890b2dea7e83816c938fc16274bf78d370f571e211d00d9a3c513f281 + languageName: node + linkType: hard + +"eslint-plugin-promise@npm:^6.0.1": + version: 6.0.1 + resolution: "eslint-plugin-promise@npm:6.0.1" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: c1bb3c2e591787e97133dcaf764f908420a3a1959a3132e199db8f14d70dfa79fc9caf991ca60a4b60ae5f1f9823bc96c2e52304828a4278ef2f3964fe121de9 languageName: node linkType: hard @@ -8030,6 +8297,45 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-typescript-sort-keys@npm:^2.1.0": + version: 2.1.0 + resolution: "eslint-plugin-typescript-sort-keys@npm:2.1.0" + dependencies: + "@typescript-eslint/experimental-utils": ^5.0.0 + json-schema: ^0.4.0 + natural-compare-lite: ^1.4.0 + peerDependencies: + "@typescript-eslint/parser": ^1 || ^2 || ^3 || ^4 || ^5 + eslint: ^5 || ^6 || ^7 || ^8 + typescript: ^3 || ^4 + checksum: 98ea442519ca9cdd36aa4d7d4e4f3d96d16a9287e32ea69209865ff3cd10ce33d9c4acb86ddc2cc0406a072aea319841496e66b512f79c58aaf3924d85f73683 + languageName: node + linkType: hard + +"eslint-plugin-unicorn@npm:^43.0.2": + version: 43.0.2 + resolution: "eslint-plugin-unicorn@npm:43.0.2" + dependencies: + "@babel/helper-validator-identifier": ^7.18.6 + ci-info: ^3.3.2 + clean-regexp: ^1.0.0 + eslint-utils: ^3.0.0 + esquery: ^1.4.0 + indent-string: ^4.0.0 + is-builtin-module: ^3.1.0 + lodash: ^4.17.21 + pluralize: ^8.0.0 + read-pkg-up: ^7.0.1 + regexp-tree: ^0.1.24 + safe-regex: ^2.1.1 + semver: ^7.3.7 + strip-indent: ^3.0.0 + peerDependencies: + eslint: ">=8.18.0" + checksum: 1b63eb013cbc0b3c9ef131a1e049b4b53d8e208393675d5f97d3fa83c050ebcb695a7fd210f4de1460f42f89c2ecca261280488834591d5c21e146d297a9ee2e + languageName: node + linkType: hard + "eslint-rule-docs@npm:^1.1.5": version: 1.1.235 resolution: "eslint-rule-docs@npm:1.1.235" @@ -8037,7 +8343,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^5.1.1": +"eslint-scope@npm:^5.1.0, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" dependencies: @@ -8057,6 +8363,15 @@ __metadata: languageName: node linkType: hard +"eslint-utils@npm:^2.0.0": + version: 2.1.0 + resolution: "eslint-utils@npm:2.1.0" + dependencies: + eslint-visitor-keys: ^1.1.0 + checksum: 27500938f348da42100d9e6ad03ae29b3de19ba757ae1a7f4a087bdcf83ac60949bbb54286492ca61fac1f5f3ac8692dd21537ce6214240bf95ad0122f24d71d + languageName: node + linkType: hard + "eslint-utils@npm:^3.0.0": version: 3.0.0 resolution: "eslint-utils@npm:3.0.0" @@ -8068,6 +8383,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^1.1.0": + version: 1.3.0 + resolution: "eslint-visitor-keys@npm:1.3.0" + checksum: 37a19b712f42f4c9027e8ba98c2b06031c17e0c0a4c696cd429bd9ee04eb43889c446f2cd545e1ff51bef9593fcec94ecd2c2ef89129fcbbf3adadbef520376a + languageName: node + linkType: hard + "eslint-visitor-keys@npm:^2.0.0": version: 2.1.0 resolution: "eslint-visitor-keys@npm:2.1.0" @@ -8550,9 +8872,9 @@ __metadata: linkType: hard "flatted@npm:^3.1.0": - version: 3.2.6 - resolution: "flatted@npm:3.2.6" - checksum: 33b87aa88dfa40ca6ee31d7df61712bbbad3d3c05c132c23e59b9b61d34631b337a18ff2b8dc5553acdc871ec72b741e485f78969cf006124a3f57174de29a0e + version: 3.2.7 + resolution: "flatted@npm:3.2.7" + checksum: 427633049d55bdb80201c68f7eb1cbd533e03eac541f97d3aecab8c5526f12a20ccecaeede08b57503e772c769e7f8680b37e8d482d1e5f8d7e2194687f9ea35 languageName: node linkType: hard @@ -8844,6 +9166,13 @@ __metadata: languageName: node linkType: hard +"get-own-property-symbols@npm:^0.9.5": + version: 0.9.5 + resolution: "get-own-property-symbols@npm:0.9.5" + checksum: 51763b9b5e28d2ee6ee3d760f790b5081b1fcdac0b41acb6844c4d00c5a69bb82eecf2d2145334c931892b0f342c02a3b0c56d6e7bed6d74cd2d667eb087dc14 + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -9001,6 +9330,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:7.1.7": + version: 7.1.7 + resolution: "glob@npm:7.1.7" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.0.4 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: b61f48973bbdcf5159997b0874a2165db572b368b931135832599875919c237fc05c12984e38fe828e69aa8a921eb0e8a4997266211c517c9cfaae8a93988bb8 + languageName: node + linkType: hard + "glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -9046,7 +9389,7 @@ __metadata: languageName: node linkType: hard -"globals@npm:^11.1.0": +"globals@npm:^11.1.0, globals@npm:^11.12.0": version: 11.12.0 resolution: "globals@npm:11.12.0" checksum: 67051a45eca3db904aee189dfc7cd53c20c7d881679c93f6146ddd4c9f4ab2268e68a919df740d39c71f4445d2b38ee360fc234428baea1dbdfe68bbcb46979e @@ -9062,13 +9405,6 @@ __metadata: languageName: node linkType: hard -"globalyzer@npm:0.1.0": - version: 0.1.0 - resolution: "globalyzer@npm:0.1.0" - checksum: 419a0f95ba542534fac0842964d31b3dc2936a479b2b1a8a62bad7e8b61054faa9b0a06ad9f2e12593396b9b2621cac93358d9b3071d33723fb1778608d358a1 - languageName: node - linkType: hard - "globby@npm:^11.0.1, globby@npm:^11.0.3, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" @@ -9096,13 +9432,6 @@ __metadata: languageName: node linkType: hard -"globrex@npm:^0.1.2": - version: 0.1.2 - resolution: "globrex@npm:0.1.2" - checksum: adca162494a176ce9ecf4dd232f7b802956bb1966b37f60c15e49d2e7d961b66c60826366dc2649093cad5a0d69970cfa8875bd1695b5a1a2f33dcd2aa88da3c - languageName: node - linkType: hard - "got@npm:^9.6.0": version: 9.6.0 resolution: "got@npm:9.6.0" @@ -9719,7 +10048,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0": +"ignore@npm:^5.1.1, ignore@npm:^5.2.0": version: 5.2.0 resolution: "ignore@npm:5.2.0" checksum: 6b1f926792d614f64c6c83da3a1f9c83f6196c2839aa41e1e32dd7b8d174cef2e329d75caabb62cb61ce9dc432f75e67d07d122a037312db7caa73166a1bdb77 @@ -9974,7 +10303,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.1.0, is-core-module@npm:^2.10.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": +"is-core-module@npm:^2.1.0, is-core-module@npm:^2.10.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.9.0": version: 2.10.0 resolution: "is-core-module@npm:2.10.0" dependencies: @@ -10006,15 +10335,6 @@ __metadata: languageName: node linkType: hard -"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": - version: 2.2.1 - resolution: "is-docker@npm:2.2.1" - bin: - is-docker: cli.js - checksum: 3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 - languageName: node - linkType: hard - "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -10258,15 +10578,6 @@ __metadata: languageName: node linkType: hard -"is-wsl@npm:^2.2.0": - version: 2.2.0 - resolution: "is-wsl@npm:2.2.0" - dependencies: - is-docker: ^2.0.0 - checksum: 20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 - languageName: node - linkType: hard - "is-yarn-global@npm:^0.3.0": version: 0.3.0 resolution: "is-yarn-global@npm:0.3.0" @@ -10990,6 +11301,13 @@ __metadata: languageName: node linkType: hard +"jsdoc-type-pratt-parser@npm:~3.1.0": + version: 3.1.0 + resolution: "jsdoc-type-pratt-parser@npm:3.1.0" + checksum: 2f437b57621f1e481918165f6cf0e48256628a9e510d8b3f88a2ab667bf2128bf8b94c628b57c43e78f555ca61983e9c282814703840dc091d2623992214a061 + languageName: node + linkType: hard + "jsdoc@npm:^3.6.10": version: 3.6.11 resolution: "jsdoc@npm:3.6.11" @@ -11078,7 +11396,7 @@ __metadata: languageName: node linkType: hard -"json-schema@npm:0.4.0": +"json-schema@npm:0.4.0, json-schema@npm:^0.4.0": version: 0.4.0 resolution: "json-schema@npm:0.4.0" checksum: 66389434c3469e698da0df2e7ac5a3281bcff75e797a5c127db7c5b56270e01ae13d9afa3c03344f76e32e81678337a8c912bdbb75101c62e487dc3778461d72 @@ -11108,17 +11426,6 @@ __metadata: languageName: node linkType: hard -"json5@npm:^1.0.1": - version: 1.0.1 - resolution: "json5@npm:1.0.1" - dependencies: - minimist: ^1.2.0 - bin: - json5: lib/cli.js - checksum: e76ea23dbb8fc1348c143da628134a98adf4c5a4e8ea2adaa74a80c455fc2cdf0e2e13e6398ef819bfe92306b610ebb2002668ed9fc1af386d593691ef346fc3 - languageName: node - linkType: hard - "json5@npm:^2.2.1": version: 2.2.1 resolution: "json5@npm:2.2.1" @@ -11186,7 +11493,7 @@ __metadata: languageName: node linkType: hard -"jsx-ast-utils@npm:^2.4.1 || ^3.0.0": +"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.2": version: 3.3.3 resolution: "jsx-ast-utils@npm:3.3.3" dependencies: @@ -11242,6 +11549,22 @@ __metadata: languageName: node linkType: hard +"language-subtag-registry@npm:~0.3.2": + version: 0.3.22 + resolution: "language-subtag-registry@npm:0.3.22" + checksum: 8ab70a7e0e055fe977ac16ea4c261faec7205ac43db5e806f72e5b59606939a3b972c4bd1e10e323b35d6ffa97c3e1c4c99f6553069dad2dfdd22020fa3eb56a + languageName: node + linkType: hard + +"language-tags@npm:^1.0.5": + version: 1.0.5 + resolution: "language-tags@npm:1.0.5" + dependencies: + language-subtag-registry: ~0.3.2 + checksum: c81b5d8b9f5f9cfd06ee71ada6ddfe1cf83044dd5eeefcd1e420ad491944da8957688db4a0a9bc562df4afdc2783425cbbdfd152c01d93179cf86888903123cf + languageName: node + linkType: hard + "latest-version@npm:^5.1.0": version: 5.1.0 resolution: "latest-version@npm:5.1.0" @@ -12754,6 +13077,13 @@ __metadata: languageName: node linkType: hard +"natural-compare-lite@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare-lite@npm:1.4.0" + checksum: 5222ac3986a2b78dd6069ac62cbb52a7bf8ffc90d972ab76dfe7b01892485d229530ed20d0c62e79a6b363a663b273db3bde195a1358ce9e5f779d4453887225 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -13190,6 +13520,18 @@ __metadata: languageName: node linkType: hard +"object.getownpropertydescriptors@npm:^2.1.4": + version: 2.1.4 + resolution: "object.getownpropertydescriptors@npm:2.1.4" + dependencies: + array.prototype.reduce: ^1.0.4 + call-bind: ^1.0.2 + define-properties: ^1.1.4 + es-abstract: ^1.20.1 + checksum: 988c466fe49fc4f19a28d2d1d894c95c6abfe33c94674ec0b14d96eed71f453c7ad16873d430dc2acbb1760de6d3d2affac4b81237a306012cc4dc49f7539e7f + languageName: node + linkType: hard + "object.hasown@npm:^1.1.1": version: 1.1.1 resolution: "object.hasown@npm:1.1.1" @@ -13250,17 +13592,6 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.4.0": - version: 8.4.0 - resolution: "open@npm:8.4.0" - dependencies: - define-lazy-prop: ^2.0.0 - is-docker: ^2.1.1 - is-wsl: ^2.2.0 - checksum: e9545bec64cdbf30a0c35c1bdc310344adf8428a117f7d8df3c0af0a0a24c513b304916a6d9b11db0190ff7225c2d578885080b761ed46a3d5f6f1eebb98b63c - languageName: node - linkType: hard - "optionator@npm:^0.9.1": version: 0.9.1 resolution: "optionator@npm:0.9.1" @@ -13664,6 +13995,13 @@ __metadata: languageName: node linkType: hard +"pluralize@npm:^8.0.0": + version: 8.0.0 + resolution: "pluralize@npm:8.0.0" + checksum: 08931d4a6a4a5561a7f94f67a31c17e6632cb21e459ab3ff4f6f629d9a822984cf8afef2311d2005fbea5d7ef26016ebb090db008e2d8bce39d0a9a9d218736e + languageName: node + linkType: hard + "postcss@npm:8.4.14": version: 8.4.14 resolution: "postcss@npm:8.4.14" @@ -14285,6 +14623,15 @@ __metadata: languageName: node linkType: hard +"regexp-tree@npm:^0.1.24, regexp-tree@npm:~0.1.1": + version: 0.1.24 + resolution: "regexp-tree@npm:0.1.24" + bin: + regexp-tree: bin/regexp-tree + checksum: 5807013289d9205288d665e0f8d8cff94843dfd55fdedd1833eb9d9bbd07188a37dfa02942ec5cdc671180037f715148fac1ba6f18fd6be4268e5a8feb49d340 + languageName: node + linkType: hard + "regexp.prototype.flags@npm:^1.4.1, regexp.prototype.flags@npm:^1.4.3": version: 1.4.3 resolution: "regexp.prototype.flags@npm:1.4.3" @@ -14296,7 +14643,7 @@ __metadata: languageName: node linkType: hard -"regexpp@npm:^3.2.0": +"regexpp@npm:^3.0.0, regexpp@npm:^3.2.0": version: 3.2.0 resolution: "regexpp@npm:3.2.0" checksum: a78dc5c7158ad9ddcfe01aa9144f46e192ddbfa7b263895a70a5c6c73edd9ce85faf7c0430e59ac38839e1734e275b9c3de5c57ee3ab6edc0e0b1bdebefccef8 @@ -14543,7 +14890,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.0, resolve@npm:^1.22.1, resolve@npm:^1.3.2": +"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.3.2": version: 1.22.1 resolution: "resolve@npm:1.22.1" dependencies: @@ -14588,7 +14935,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.3.2#~builtin": +"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.3.2#~builtin": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=07638b" dependencies: @@ -14820,6 +15167,15 @@ __metadata: languageName: node linkType: hard +"safe-regex@npm:^2.1.1": + version: 2.1.1 + resolution: "safe-regex@npm:2.1.1" + dependencies: + regexp-tree: ~0.1.1 + checksum: 5d734e2193c63ef0cb00f60c0244e0f8a30ecb31923633cd34636808d6a7c4c206d650017953ae1db8bc33967c2f06af33488dea6f038f4e38212beb7bed77b4 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -15217,7 +15573,7 @@ __metadata: languageName: node linkType: hard -"spdx-expression-parse@npm:^3.0.0": +"spdx-expression-parse@npm:^3.0.0, spdx-expression-parse@npm:^3.0.1": version: 3.0.1 resolution: "spdx-expression-parse@npm:3.0.1" dependencies: @@ -15682,16 +16038,6 @@ __metadata: languageName: node linkType: hard -"synckit@npm:^0.8.3": - version: 0.8.3 - resolution: "synckit@npm:0.8.3" - dependencies: - "@pkgr/utils": ^2.3.0 - tslib: ^2.4.0 - checksum: ba6baa7c32e69b38eebb322a8eb1712a117f7f1eaa42e75623c7d6da43f0e61d3ac33fa962c2c58a8b37742f1e0ae9d21f04c9da0dbf34d618a8780055e9e1fa - languageName: node - linkType: hard - "table-layout@npm:^0.4.2": version: 0.4.5 resolution: "table-layout@npm:0.4.5" @@ -15712,13 +16058,6 @@ __metadata: languageName: node linkType: hard -"tapable@npm:^2.2.0": - version: 2.2.1 - resolution: "tapable@npm:2.2.1" - checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 - languageName: node - linkType: hard - "tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.1": version: 2.1.1 resolution: "tar-fs@npm:2.1.1" @@ -15911,16 +16250,6 @@ __metadata: languageName: node linkType: hard -"tiny-glob@npm:^0.2.9": - version: 0.2.9 - resolution: "tiny-glob@npm:0.2.9" - dependencies: - globalyzer: 0.1.0 - globrex: ^0.1.2 - checksum: aea5801eb6663ddf77ebb74900b8f8bd9dfcfc9b6a1cc8018cb7421590c00bf446109ff45e4b64a98e6c95ddb1255a337a5d488fb6311930e2a95334151ec9c6 - languageName: node - linkType: hard - "tinypool@npm:^0.2.4": version: 0.2.4 resolution: "tinypool@npm:0.2.4" @@ -16120,18 +16449,6 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.14.1": - version: 3.14.1 - resolution: "tsconfig-paths@npm:3.14.1" - dependencies: - "@types/json5": ^0.0.29 - json5: ^1.0.1 - minimist: ^1.2.6 - strip-bom: ^3.0.0 - checksum: 8afa01c673ebb4782ba53d3a12df97fa837ce524f8ad38ee4e2b2fd57f5ac79abc21c574e9e9eb014d93efe7fe8214001b96233b5c6ea75bd1ea82afe17a4c6d - languageName: node - linkType: hard - "tsd@npm:^0.23.0": version: 0.23.0 resolution: "tsd@npm:0.23.0"