Compare commits

...

21 Commits

Author SHA1 Message Date
renovate[bot]
dabcb34338 chore(deps): lock file maintenance 2026-06-01 18:01:45 +00:00
github-actions[bot]
fb6e8c2cfc chore(main): release 4.2.6 (#403)
Some checks failed
Continuous Delivery / Publishing Dev (push) Has been cancelled
NPM / Publish / test-and-publish (push) Has been cancelled
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-22 16:52:08 -05:00
Duro
0eecb08e87 chore: release 4.2.6 (#402)
Release-As: 4.2.6
2025-09-22 16:40:12 -05:00
Duro
c67748c7df fix ready deprecation (#401)
* chore: bump discord.js version

* fix: change ready event to clientReady to fix deprecation warning

* fix: fix compatibility for older discord.js versions

* chore: bump package version
2025-09-22 11:23:07 -05:00
github-actions[bot]
efee0fdbe2 chore(main): release 4.2.5 (#400) 2025-08-31 14:27:57 -05:00
Duro
797442ece3 fix: make message module warn rather than throwing (#399) 2025-08-31 14:23:39 -05:00
renovate[bot]
513ac8edf4 chore(deps): lock file maintenance (#391)
Some checks failed
NPM / Publish / test-and-publish (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 00:46:28 -05:00
github-actions[bot]
81a0180d05 chore(main): release 4.2.4 (#396)
Some checks failed
Continuous Delivery / Publishing Dev (push) Has been cancelled
NPM / Publish / test-and-publish (push) Has been cancelled
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-05 21:24:41 -06:00
Jacob Nguyen
89d7409536 fix: flat autocomplete (#395)
* first

* fix
2025-03-05 21:22:54 -06:00
github-actions[bot]
aa802f761e chore(main): release 4.2.3 (#394)
Some checks failed
Continuous Delivery / Publishing Dev (push) Has been cancelled
NPM / Publish / test-and-publish (push) Has been cancelled
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-03 21:48:03 -06:00
Jacob Nguyen
2414992b73 fix: autocomplete sdt.module not present (#393) 2025-03-03 21:45:18 -06:00
kingomes
70c6236802 Update README.md (#392)
Some checks failed
NPM / Publish / test-and-publish (push) Has been cancelled
2025-02-15 11:09:34 -06:00
Jacob Nguyen
1f25aa64b9 plock
Some checks failed
Continuous Delivery / Publishing Dev (push) Has been cancelled
NPM / Publish / test-and-publish (push) Has been cancelled
2025-02-07 01:45:57 -06:00
Jacob Nguyen
7cddee30aa lock 2025-02-07 01:44:49 -06:00
renovate[bot]
e7286eee9f chore(deps): lock file maintenance (#331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-07 01:43:27 -06:00
Jacob Nguyen
a67450328e fuckyarn 2025-02-07 00:50:17 -06:00
Jacob Nguyen
47401f46a3 Update README.md
Some checks failed
NPM / Publish / test-and-publish (push) Has been cancelled
2025-02-03 11:47:49 -06:00
github-actions[bot]
1059065980 chore(main): release 4.2.2 (#388)
Some checks failed
NPM / Publish / test-and-publish (push) Waiting to run
Continuous Delivery / Publishing Dev (push) Has been cancelled
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-02 19:40:19 -06:00
Jacob Nguyen
974c30fa6c fix: faster autocomplete lookup (#387)
* fix:faster-autocmp

* fixinitializing

* fix

* fixonwindows

* unconsole
2025-02-02 19:37:59 -06:00
Jacob Nguyen
3a569726d8 docs: Sincetagsandmoredocumentation (#385)
Some checks failed
Continuous Delivery / Publishing Dev (push) Has been cancelled
NPM / Publish / test-and-publish (push) Has been cancelled
* commit

* docs
2025-01-27 18:29:20 -06:00
Ararou
1b7f2a49a8 change formatting of example projects (#384)
Some checks failed
NPM / Publish / test-and-publish (push) Has been cancelled
2025-01-25 22:26:31 -06:00
17 changed files with 4619 additions and 4586 deletions

View File

@@ -13,9 +13,9 @@ jobs:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with: with:
node-version: 17 node-version: 18
- run: yarn --immutable - run: npm i
- run: yarn build:prod - run: npm run build:prod
- uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1 - uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
with: with:
token: ${{ secrets.NPM_TOKEN }} token: ${{ secrets.NPM_TOKEN }}

View File

@@ -24,6 +24,5 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm' cache: 'npm'
- run: npm install -g yarn - run: npm install
- run: yarn install - run: npm run test
- run: yarn test

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
enableGlobalCache: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.5.1.cjs

View File

@@ -1,5 +1,40 @@
# Changelog # Changelog
## [4.2.6](https://github.com/sern-handler/handler/compare/v4.2.5...v4.2.6) (2025-09-22)
### Miscellaneous Chores
* release 4.2.6 ([#402](https://github.com/sern-handler/handler/issues/402)) ([0eecb08](https://github.com/sern-handler/handler/commit/0eecb08e87e26102030ccf6ef38ddd81051ec373))
## [4.2.5](https://github.com/sern-handler/handler/compare/v4.2.4...v4.2.5) (2025-08-31)
### Bug Fixes
* make message module warn rather than throwing ([#399](https://github.com/sern-handler/handler/issues/399)) ([797442e](https://github.com/sern-handler/handler/commit/797442ece3999bf2cb6b5ba0688ce0177e72a22f))
## [4.2.4](https://github.com/sern-handler/handler/compare/v4.2.3...v4.2.4) (2025-03-06)
### Bug Fixes
* flat autocomplete ([#395](https://github.com/sern-handler/handler/issues/395)) ([89d7409](https://github.com/sern-handler/handler/commit/89d74095363befddc3222b9e5c89c35e7c6457b9))
## [4.2.3](https://github.com/sern-handler/handler/compare/v4.2.2...v4.2.3) (2025-03-04)
### Bug Fixes
* autocomplete sdt.module not present ([#393](https://github.com/sern-handler/handler/issues/393)) ([2414992](https://github.com/sern-handler/handler/commit/2414992b73a40065464b20f2d53826c78fcd3a5f))
## [4.2.2](https://github.com/sern-handler/handler/compare/v4.2.1...v4.2.2) (2025-02-03)
### Bug Fixes
* faster autocomplete lookup ([#387](https://github.com/sern-handler/handler/issues/387)) ([974c30f](https://github.com/sern-handler/handler/commit/974c30fa6cccaae7b1c2c3246ffa9eecb6bc7bf9))
## [4.2.1](https://github.com/sern-handler/handler/compare/v4.2.0...v4.2.1) (2025-01-24) ## [4.2.1](https://github.com/sern-handler/handler/compare/v4.2.0...v4.2.1) (2025-01-24)

View File

@@ -7,6 +7,7 @@
<div align="center" styles="margin-top: 10px"> <div align="center" styles="margin-top: 10px">
<img src="https://img.shields.io/badge/open-source-brightgreen" /> <img src="https://img.shields.io/badge/open-source-brightgreen" />
<img src="https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev"/>
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/v/@sern/handler?maxAge=3600" alt="NPM version" /></a> <a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/v/@sern/handler?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/dt/@sern/handler?maxAge=3600" alt="NPM downloads" /></a> <a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/dt/@sern/handler?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="License MIT" /></a> <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="License MIT" /></a>
@@ -19,7 +20,7 @@
- Lightweight. Does a lot while being small. - Lightweight. Does a lot while being small.
- Latest features. Support for discord.js v14 and all of its interactions. - Latest features. Support for discord.js v14 and all of its interactions.
- Start quickly. Plug and play or customize to your liking. - Start quickly. Plug and play or customize to your liking.
- works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box! - Works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
- Use it with TypeScript or JavaScript. CommonJS and ESM supported. - Use it with TypeScript or JavaScript. CommonJS and ESM supported.
- Active and growing community, always here to help. [Join us](https://sern.dev/discord) - Active and growing community, always here to help. [Join us](https://sern.dev/discord)
- Unleash its full potential with a powerful CLI and awesome plugins. - Unleash its full potential with a powerful CLI and awesome plugins.
@@ -43,20 +44,29 @@ export default commandModule({
``` ```
</details> </details>
# Show off your sern Discord Bot!
## Badge
- Copy this and add it to your [README.md](https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev)
<img src="https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev">
## 🤖 Bots Using sern ## 🤖 Bots Using sern
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord). - [Community Bot](https://github.com/sern-handler/sern-community) - The community bot for our [Discord server](https://sern.dev/discord).
- [Vinci](https://github.com/SrIzan10/vinci), the bot for Mara Turing. - [Vinci](https://github.com/SrIzan10/vinci) - The bot for Mara Turing.
- [Bask](https://github.com/baskbotml/bask), Listen your favorite artists on Discord. - [Bask](https://github.com/baskbotml/bask) - Listen to your favorite artists on Discord.
- [Murayama](https://github.com/murayamabot/murayama), :pepega: - [Murayama](https://github.com/murayamabot/murayama) - :pepega:
- [Protector](https://github.com/GlitchApotamus/Protector), Just a simple bot to help enhance a private minecraft server. - [Protector](https://github.com/GlitchApotamus/Protector) - Just a simple bot to help enhance a private Minecraft server.
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud), A fun bot for a small - but growing - server. - [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud) - A fun bot for a small, but growing server.
- [Man Nomic](https://github.com/jacoobes/man-nomic), A simple information bot to provide information to the nomic-ai discord community. - [Man Nomic](https://github.com/jacoobes/man-nomic) - A simple information bot to provide information to the nomic-ai Discord community.
- [Linear-Discord](https://github.com/sern-handler/linear-discord) Display and manage a linear dashboard. - [Linear-Discord](https://github.com/sern-handler/linear-discord) - Display and manage a linear dashboard.
- [ZenithBot](https://github.com/CodeCraftersHaven/ZenithBot) - A versatile bot coded in TypeScript, designed to enhance server management and user interaction through its robust features.
## 💻 CLI ## 💻 CLI
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it. It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.
## 🔗 Links ## 🔗 Links
- [Official Documentation and Guide](https://sern.dev) - [Official Documentation and Guide](https://sern.dev)

4050
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "@sern/handler", "name": "@sern/handler",
"packageManager": "yarn@3.5.0", "packageManager": "yarn@3.5.0",
"version": "4.2.1", "version": "4.2.6",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.", "description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -20,6 +20,7 @@
"prepare": "tsc", "prepare": "tsc",
"pretty": "prettier --write .", "pretty": "prettier --write .",
"tdd": "vitest", "tdd": "vitest",
"benchmark": "vitest bench",
"test": "vitest --run", "test": "vitest --run",
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg" "analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
}, },
@@ -35,7 +36,7 @@
"author": "SernDevs", "author": "SernDevs",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sern/ioc": "^1.1.0", "@sern/ioc": "^1.1.2",
"callsites": "^3.1.0", "callsites": "^3.1.0",
"cron": "^3.1.7", "cron": "^3.1.7",
"deepmerge": "^4.3.1" "deepmerge": "^4.3.1"
@@ -46,7 +47,7 @@
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@typescript-eslint/eslint-plugin": "5.58.0", "@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1", "@typescript-eslint/parser": "5.59.1",
"discord.js": "^14.15.3", "discord.js": "^14.22.1",
"eslint": "8.39.0", "eslint": "8.39.0",
"typescript": "5.0.2", "typescript": "5.0.2",
"vitest": "^1.6.0" "vitest": "^1.6.0"

View File

@@ -6,12 +6,12 @@ import type {
MessageContextMenuCommandInteraction, MessageContextMenuCommandInteraction,
ModalSubmitInteraction, ModalSubmitInteraction,
UserContextMenuCommandInteraction, UserContextMenuCommandInteraction,
AutocompleteInteraction AutocompleteInteraction,
} from 'discord.js'; } from 'discord.js';
import { ApplicationCommandOptionType, InteractionType } from 'discord.js'; import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
import { PluginType } from './structures/enums'; import { PluginType } from './structures/enums';
import assert from 'assert';
import type { Payload, UnpackedDependencies } from '../types/utility'; import type { Payload, UnpackedDependencies } from '../types/utility';
import path from 'node:path'
export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => { export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
return { return {
@@ -57,51 +57,31 @@ export function partitionPlugins<T,V>
return [controlPlugins, initPlugins] as [T[], V[]]; return [controlPlugins, initPlugins] as [T[], V[]];
} }
/** export const createLookupTable = (options: SernOptionsData[]): Map<string, SernAutocompleteData> => {
* Uses an iterative DFS to check if an autocomplete node exists on the option tree const table = new Map<string, SernAutocompleteData>();
* @param iAutocomplete _createLookupTable(table, options, "<parent>");
* @param options return table;
*/ }
export function treeSearch(
iAutocomplete: AutocompleteInteraction, const _createLookupTable = (table: Map<string, SernAutocompleteData>, options: SernOptionsData[], parent: string) => {
options: SernOptionsData[] | undefined, for (const opt of options) {
): SernAutocompleteData & { parent?: string } | undefined { const name = path.posix.join(parent, opt.name)
if (options === undefined) return undefined; switch(opt.type) {
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
const subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand: { case ApplicationCommandOptionType.Subcommand: {
subcommands.add(cur.name); _createLookupTable(table, opt.options ?? [], name);
for (const option of cur.options ?? []) _options.push(option); } break;
} break;
case ApplicationCommandOptionType.SubcommandGroup: { case ApplicationCommandOptionType.SubcommandGroup: {
for (const command of cur.options ?? []) _options.push(command); _createLookupTable(table, opt.options ?? [], name);
} break; } break;
default: { default: {
if ('autocomplete' in cur && cur.autocomplete) { if(Reflect.get(opt, 'autocomplete') === true) {
const choice = iAutocomplete.options.getFocused(true); table.set(name, opt as SernAutocompleteData)
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
if (subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
} }
} break; } break;
} }
} }
}
}
interface InteractionTypable { interface InteractionTypable {
type: InteractionType; type: InteractionType;

View File

@@ -1,10 +1,11 @@
import type { Module } from '../types/core-modules' import type { Module, SernAutocompleteData } from '../types/core-modules'
import { callPlugins, executeModule } from './event-utils'; import { callPlugins, executeModule } from './event-utils';
import { SernError } from '../core/structures/enums' import { SernError } from '../core/structures/enums'
import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload, treeSearch } from '../core/functions' import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
import type { UnpackedDependencies } from '../types/utility'; import type { UnpackedDependencies } from '../types/utility';
import * as Id from '../core/id' import * as Id from '../core/id'
import { Context } from '../core/structures/context'; import { Context } from '../core/structures/context';
import path from 'node:path';
@@ -31,10 +32,15 @@ export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: s
let payload; let payload;
// handles autocomplete // handles autocomplete
if(isAutocomplete(event)) { if(isAutocomplete(event)) {
//@ts-ignore stfu const lookupTable = module.locals['@sern/lookup-table'] as Map<string, SernAutocompleteData>
const { command } = treeSearch(event, module.options); const subCommandGroup = event.options.getSubcommandGroup(false) ?? "",
payload= { module: command as Module, //autocomplete is not a true "module" warning cast! subCommand = event.options.getSubcommand(false) ?? "",
args: [event, createSDT(command, deps, params)] }; option = event.options.getFocused(true),
fullPath = path.posix.join("<parent>", subCommandGroup, subCommand, option.name)
const resolvedModule = (lookupTable.get(fullPath)!.command) as Module
payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast!
args: [event, createSDT(module, deps, params)] };
// either CommandTypes Slash | ContextMessage | ContextUesr // either CommandTypes Slash | ContextMessage | ContextUesr
} else if(isCommand(event)) { } else if(isCommand(event)) {
const sdt = createSDT(module, deps, params) const sdt = createSDT(module, deps, params)

View File

@@ -36,7 +36,7 @@ export function messageHandler (deps: UnpackedDependencies, defaultPrefix?: stri
const [prefix] = fmt(message.content, defaultPrefix); const [prefix] = fmt(message.content, defaultPrefix);
let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module; let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
if(!module) { if(!module) {
throw Error('Possibly undefined behavior: could not find a static id to resolve') log?.warning({ message: 'Possibly undefined behavior: could not find a static id to resolve' });
} }
const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] } const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] }
const result = await callPlugins(payload) const result = await callPlugins(payload)

View File

@@ -1,37 +1,58 @@
import * as Files from '../core/module-loading' import * as Files from "../core/module-loading";
import { once } from 'node:events'; import { once } from "node:events";
import { resultPayload } from '../core/functions'; import { createLookupTable, resultPayload } from "../core/functions";
import { CommandType } from '../core/structures/enums'; import { CommandType } from "../core/structures/enums";
import { Module } from '../types/core-modules'; import { Module, SernOptionsData } from "../types/core-modules";
import type { UnpackedDependencies, Wrapper } from '../types/utility'; import type { UnpackedDependencies, Wrapper } from "../types/utility";
import { callInitPlugins } from './event-utils'; import { callInitPlugins } from "./event-utils";
import { SernAutocompleteData } from "..";
import { Events } from "discord.js";
export default async function(dirs: string | string[], deps : UnpackedDependencies) { export default async function (
const { '@sern/client': client, dirs: string | string[],
'@sern/logger': log, deps: UnpackedDependencies,
'@sern/emitter': sEmitter, ) {
'@sern/modules': commands } = deps; const {
log?.info({ message: "Waiting on discord client to be ready..." }) "@sern/client": client,
await once(client, "ready"); "@sern/logger": log,
log?.info({ message: "Client signaled ready, registering modules" }); "@sern/emitter": sEmitter,
"@sern/modules": commands,
} = deps;
log?.info({ message: "Waiting on discord client to be ready..." });
await once(client, Events.ClientReady);
log?.info({ message: "Client signaled ready, registering modules" });
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator // https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
// possibly optimize to concurrently import modules // possibly optimize to concurrently import modules
const directories = Array.isArray(dirs) ? dirs : [dirs]; const directories = Array.isArray(dirs) ? dirs : [dirs];
for (const dir of directories) { for (const dir of directories) {
for await (const path of Files.readRecursive(dir)) { for await (const path of Files.readRecursive(dir)) {
let { module } = await Files.importModule<Module>(path); const { module } = await Files.importModule<Module>(path);
const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect; const validType =
if(!validType) { module.type >= CommandType.Text &&
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``); module.type <= CommandType.ChannelSelect;
} if (!validType) {
const resultModule = await callInitPlugins(module, deps, true); throw Error(
// FREEZE! no more writing!! `Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``,
commands.set(resultModule.meta.id, Object.freeze(resultModule)); );
sEmitter.emit('module.register', resultPayload('success', resultModule)); }
} const resultModule = await callInitPlugins(module, deps, true);
if (
module.type === CommandType.Both ||
module.type === CommandType.Slash
) {
const options = (Reflect.get(module, "options") ??
[]) as SernOptionsData[];
const lookupTable = createLookupTable(options);
module.locals["@sern/lookup-table"] = lookupTable;
}
// FREEZE! no more writing!!
commands.set(resultModule.meta.id, Object.freeze(resultModule));
sEmitter.emit("module.register", resultPayload("success", resultModule));
} }
sEmitter.emit('modulesLoaded'); }
sEmitter.emit("modulesLoaded");
} }

View File

@@ -61,7 +61,7 @@ import { Awaitable, SernEventsMapping, UnpackedDependencies, Dictionary } from '
* *
* @see {@link CommandControlPlugin} for plugin implementation * @see {@link CommandControlPlugin} for plugin implementation
* @see {@link CommandType} for available command types * @see {@link CommandType} for available command types
* @see {@link Dependencies} for dependency injection interface * @see {@link Dependencies} for [dependency injection](https://sern.dev/v4/reference/dependencies/) interface
*/ */
export type SDT = { export type SDT = {
/** /**
@@ -114,6 +114,10 @@ export type SDT = {
export type Processed<T> = T & { name: string; description: string }; export type Processed<T> = T & { name: string; description: string };
/**
* @since 1.0.0
*/
export interface Module { export interface Module {
type: CommandType | EventType; type: CommandType | EventType;
name?: string; name?: string;
@@ -196,13 +200,18 @@ export interface Module {
execute(...args: any[]): Awaitable<any>; execute(...args: any[]): Awaitable<any>;
} }
/**
* @since 1.0.0
*/
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping> export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
extends Module { extends Module {
name?: T; name?: T;
type: EventType.Sern; type: EventType.Sern;
execute(...args: SernEventsMapping[T]): Awaitable<unknown>; execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface ExternalEventCommand extends Module { export interface ExternalEventCommand extends Module {
name?: string; name?: string;
emitter: keyof Dependencies; emitter: keyof Dependencies;
@@ -210,83 +219,121 @@ export interface ExternalEventCommand extends Module {
execute(...args: unknown[]): Awaitable<unknown>; execute(...args: unknown[]): Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface ContextMenuUser extends Module { export interface ContextMenuUser extends Module {
type: CommandType.CtxUser; type: CommandType.CtxUser;
execute: (ctx: UserContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: UserContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface ContextMenuMsg extends Module { export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg; type: CommandType.CtxMsg;
execute: (ctx: MessageContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: MessageContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface ButtonCommand extends Module { export interface ButtonCommand extends Module {
type: CommandType.Button; type: CommandType.Button;
execute: (ctx: ButtonInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: ButtonInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface StringSelectCommand extends Module { export interface StringSelectCommand extends Module {
type: CommandType.StringSelect; type: CommandType.StringSelect;
execute: (ctx: StringSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: StringSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface ChannelSelectCommand extends Module { export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect; type: CommandType.ChannelSelect;
execute: (ctx: ChannelSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: ChannelSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface RoleSelectCommand extends Module { export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect; type: CommandType.RoleSelect;
execute: (ctx: RoleSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: RoleSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface MentionableSelectCommand extends Module { export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect; type: CommandType.MentionableSelect;
execute: (ctx: MentionableSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: MentionableSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface UserSelectCommand extends Module { export interface UserSelectCommand extends Module {
type: CommandType.UserSelect; type: CommandType.UserSelect;
execute: (ctx: UserSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: UserSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface ModalSubmitCommand extends Module { export interface ModalSubmitCommand extends Module {
type: CommandType.Modal; type: CommandType.Modal;
execute: (ctx: ModalSubmitInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: ModalSubmitInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface AutocompleteCommand { export interface AutocompleteCommand {
onEvent?: ControlPlugin[]; onEvent?: ControlPlugin[];
execute: (ctx: AutocompleteInteraction, tbd: SDT) => Awaitable<unknown>; execute: (ctx: AutocompleteInteraction, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents> export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
extends Module { extends Module {
name?: T; name?: T;
type: EventType.Discord; type: EventType.Discord;
execute(...args: ClientEvents[T]): Awaitable<unknown>; execute(...args: ClientEvents[T]): Awaitable<unknown>;
} }
/**
* @since 1.0.0
* @see @link {commandModule} to create a text command
*/
export interface TextCommand extends Module { export interface TextCommand extends Module {
type: CommandType.Text; type: CommandType.Text;
execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable<unknown>; execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
* @see @link {commandModule} to create a slash command
*/
export interface SlashCommand extends Module { export interface SlashCommand extends Module {
type: CommandType.Slash; type: CommandType.Slash;
description: string; description: string;
options?: SernOptionsData[]; options?: SernOptionsData[];
execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable<unknown>; execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
* @see @link {commandModule} to create a both command
*/
export interface BothCommand extends Module { export interface BothCommand extends Module {
type: CommandType.Both; type: CommandType.Both;
description: string; description: string;
options?: SernOptionsData[]; options?: SernOptionsData[];
execute: (ctx: Context, tbd: SDT) => Awaitable<unknown>; execute: (ctx: Context, tbd: SDT) => Awaitable<unknown>;
} }
/**
* @since 1.0.0
*/
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand; export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
/**
* @since 1.0.0
*/
export type CommandModule = export type CommandModule =
| TextCommand | TextCommand
| SlashCommand | SlashCommand
@@ -355,6 +402,7 @@ export type InputCommand = {
}[CommandType]; }[CommandType];
/** /**
* @see @link {https://sern.dev/v4/reference/autocomplete/}
* Type that replaces autocomplete with {@link SernAutocompleteData} * Type that replaces autocomplete with {@link SernAutocompleteData}
*/ */
export type SernOptionsData = export type SernOptionsData =
@@ -374,7 +422,9 @@ export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsDa
options?: SernSubCommandData[]; options?: SernSubCommandData[];
} }
/**
* @since 4.0.0
*/
export interface ScheduledTaskContext { export interface ScheduledTaskContext {
/** /**
@@ -398,7 +448,9 @@ interface TaskAttrs {
*/ */
deps: UnpackedDependencies deps: UnpackedDependencies
} }
/**
* @since 4.0.0
*/
export interface ScheduledTask { export interface ScheduledTask {
name?: string; name?: string;
trigger: string | Date; trigger: string | Date;

84
test/autocomp.bench.ts Normal file
View File

@@ -0,0 +1,84 @@
import { describe } from 'node:test'
import { bench } from 'vitest'
import { SernAutocompleteData, SernOptionsData } from '../src'
import { createRandomChoice } from './setup/util'
import { ApplicationCommandOptionType, AutocompleteFocusedOption, AutocompleteInteraction } from 'discord.js'
import { createLookupTable } from '../src/core/functions'
import assert from 'node:assert'
/**
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
* This is the old internal method that sern used to resolve autocomplete
* @param iAutocomplete
* @param options
*/
function treeSearch(
choice: AutocompleteFocusedOption,
parent: string|undefined,
options: SernOptionsData[] | undefined,
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
const subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand: {
subcommands.add(cur.name);
for (const option of cur.options ?? []) _options.push(option);
} break;
case ApplicationCommandOptionType.SubcommandGroup: {
for (const command of cur.options ?? []) _options.push(command);
} break;
default: {
if ('autocomplete' in cur && cur.autocomplete) {
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
if (subcommands.size > 0) {
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
}
} break;
}
}
}
const options: SernOptionsData[] = [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'autocomplete',
description: 'here',
autocomplete: true,
command: { onEvent: [], execute: () => {} },
},
]
const table = createLookupTable(options)
describe('autocomplete lookup', () => {
bench('lookup table', () => {
table.get('<parent>/autocomplete')
}, { time: 500 })
bench('naive treeSearch', () => {
treeSearch({ focused: true,
name: 'autocomplete',
value: 'autocomplete',
type: ApplicationCommandOptionType.String }, undefined, options)
}, { time: 500 })
})

View File

@@ -1,29 +1,17 @@
//@ts-nocheck //@ts-nocheck
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { PluginType, SernOptionsData, controller } from '../../src/index'; import { PluginType, SernOptionsData, controller } from '../../src/index';
import { partitionPlugins, treeSearch } from '../../src/core/functions'; import { createLookupTable, partitionPlugins, treeSearch } from '../../src/core/functions';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js'; import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import { createRandomChoice, createRandomPlugins } from '../setup/util';
describe('functions', () => { describe('functions', () => {
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
function createRandomPlugins(len: number) {
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
return Array.from({ length: len }, () => ({
type: random(),
execute: () => (random() === 1 ? controller.next() : controller.stop()),
}));
}
function createRandomChoice() {
return {
type: faker.number.int({ min: 1, max: 11 }),
name: faker.word.noun(),
description: faker.word.adjective(),
};
}
it('should partition plugins correctly', () => { it('should partition plugins correctly', () => {
const plugins = createRandomPlugins(100); const plugins = createRandomPlugins(100);
const [onEvent, init] = partitionPlugins(plugins); const [onEvent, init] = partitionPlugins(plugins);
@@ -32,308 +20,275 @@ describe('functions', () => {
for (const el of init) expect(el.type).to.equal(PluginType.Init); for (const el of init) expect(el.type).to.equal(PluginType.Init);
}); });
it('should tree search options tree depth 1', () => { describe('autocomplete', ( ) => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('autocomplete'); it('should tree search options tree depth 1', () => {
const options: SernOptionsData[] = [ const options: SernOptionsData[] = [
createRandomChoice(), createRandomChoice(),
createRandomChoice(), {
createRandomChoice(), type: ApplicationCommandOptionType.String,
{ name: 'autocomplete',
type: ApplicationCommandOptionType.String, description: 'here',
name: 'autocomplete', autocomplete: true,
description: 'here', command: { onEvent: [], execute: vi.fn() },
autocomplete: true, },
command: { onEvent: [], execute: vi.fn() }, ];
}, const table = createLookupTable(options)
]; const result = table.get('<parent>/autocomplete')
autocmpInteraction.options.getFocused.mockReturnValue({ expect(result == undefined).to.be.false;
name: 'autocomplete', expect(result.name).to.be.eq('autocomplete');
value: faker.string.alpha(), expect(result.command).to.be.not.undefined;
focused: true, }),
}); it('should tree search depth 2', () => {
const result = treeSearch(autocmpInteraction, options); const subcommandName = faker.string.alpha();
expect(result == undefined).to.be.false; const options: SernOptionsData[] = [
expect(result.name).to.be.eq('autocomplete'); {
expect(result.command).to.be.not.undefined; type: ApplicationCommandOptionType.Subcommand,
}), name: subcommandName,
it('should tree search depth 2', () => { description: faker.string.alpha(),
//@ts-expect-error mocking options: [
let autocmpInteraction = new AutocompleteInteraction('nested'); createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
];
const table = createLookupTable(options)
const result = table.get(`<parent>/${subcommandName}/nested`)
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('should tree search depth n > 2', () => {
const subgroupName = faker.string.alpha()
const subcommandName = faker.string.alpha(); const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [ const options: SernOptionsData[] = [
{ {
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.SubcommandGroup,
name: subcommandName, name: subgroupName,
description: faker.string.alpha(), description: faker.string.alpha(),
options: [ options: [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{ {
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.Subcommand,
name: 'nested', name: subcommandName,
description: faker.string.alpha(), description: faker.string.alpha(),
autocomplete: true, options: [
command: { createRandomChoice(),
onEvent: [], createRandomChoice(),
execute: () => {}, {
}, type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
createRandomChoice(),
],
}, },
], ],
}, },
]; ];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName); const table = createLookupTable(options)
autocmpInteraction.options.getFocused.mockReturnValue({ const result = table.get(`<parent>/${subgroupName}/${subcommandName}/nested`)
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false; expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested'); expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined; expect(result.command).to.be.not.undefined;
}); });
it('should tree search depth n > 2', () => { it('should correctly resolve suboption of the same name given two subcommands ', () => {
//@ts-expect-error mocking const subcommandName = faker.string.alpha();
let autocmpInteraction = new AutocompleteInteraction('nested'); const groupname = faker.string.alpha()
const subcommandName = faker.string.alpha(); const options: SernOptionsData[] = [
const options: SernOptionsData[] = [ {
{ type: ApplicationCommandOptionType.SubcommandGroup,
type: ApplicationCommandOptionType.SubcommandGroup, name: groupname,
name: faker.string.alpha(), description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ {
{ type: ApplicationCommandOptionType.Subcommand,
type: ApplicationCommandOptionType.Subcommand, name: subcommandName,
name: subcommandName, description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ createRandomChoice(),
createRandomChoice(), createRandomChoice(),
createRandomChoice(), {
{ type: ApplicationCommandOptionType.String,
type: ApplicationCommandOptionType.String, name: 'nested',
name: 'nested', description: faker.string.alpha(),
description: faker.string.alpha(), autocomplete: true,
autocomplete: true, command: {
command: { onEvent: [],
onEvent: [], execute: () => {},
execute: () => {}, },
}, },
}, ],
createRandomChoice(), },
], {
}, type: ApplicationCommandOptionType.Subcommand,
], name: subcommandName + 'a',
}, description: faker.string.alpha(),
]; options: [
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName); createRandomChoice(),
autocmpInteraction.options.getFocused.mockReturnValue({ {
name: 'nested', type: ApplicationCommandOptionType.String,
value: faker.string.alpha(), name: 'nested',
focused: true, description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
const table = createLookupTable(options)
const result = table.get(`<parent>/${groupname}/${subcommandName}/nested`);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
}); });
const result = treeSearch(autocmpInteraction, options); it('two subcommands with an option of the same name', () => {
expect(result == undefined).to.be.false; const groupName = faker.string.alpha()
expect(result.name).to.be.eq('nested'); const subcommandName = faker.string.alpha();
expect(result.command).to.be.not.undefined; const options: SernOptionsData[] = [
}); {
it('should correctly resolve suboption of the same name given two subcommands ', () => { type: ApplicationCommandOptionType.SubcommandGroup,
let autocmpInteraction = new AutocompleteInteraction('nested'); name: groupName,
const subcommandName = faker.string.alpha(); description: faker.string.alpha(),
const options: SernOptionsData[] = [ options: [
{ {
type: ApplicationCommandOptionType.SubcommandGroup, type: ApplicationCommandOptionType.Subcommand,
name: faker.string.alpha(), name: subcommandName,
description: faker.string.alpha(), description: faker.string.alpha(),
options: [ options: [
{ createRandomChoice(),
type: ApplicationCommandOptionType.Subcommand, createRandomChoice(),
name: subcommandName, {
description: faker.string.alpha(), type: ApplicationCommandOptionType.String,
options: [ name: 'nested',
createRandomChoice(), description: faker.string.alpha(),
createRandomChoice(), autocomplete: true,
{ command: {
type: ApplicationCommandOptionType.String, onEvent: [],
name: 'nested', execute: () => {},
description: faker.string.alpha(), },
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
}, },
}, ],
], },
}, {
{ type: ApplicationCommandOptionType.Subcommand,
type: ApplicationCommandOptionType.Subcommand, name: subcommandName + 'anothera',
name: subcommandName + 'a', description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ createRandomChoice(),
createRandomChoice(), {
{ type: ApplicationCommandOptionType.String,
type: ApplicationCommandOptionType.String, name: 'nested',
name: 'nested', description: faker.string.alpha(),
description: faker.string.alpha(), autocomplete: true,
autocomplete: true, command: {
command: { onEvent: [],
onEvent: [], execute: () => {},
execute: () => {}, },
}, },
}, ],
], },
}, ],
], },
}, ];
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName); const table = createLookupTable(options)
autocmpInteraction.options.getFocused.mockReturnValue({ const result = table.get(`<parent>/${groupName}/${subcommandName}/nested`);
name: 'nested', expect(result).toBeTruthy();
value: faker.string.alpha(), expect(result.name).to.be.eq('nested');
focused: true, expect(result.command).to.be.not.undefined;
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('two subcommands with an option of the same name', () => {
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
let autocmpInteraction2 = new AutocompleteInteraction('nested');
autocmpInteraction2.options.getSubcommand.mockReturnValue(subcommandName + 'a');
autocmpInteraction2.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
}); });
const result2 = treeSearch(autocmpInteraction2, options);
expect(result2).toBeTruthy();
expect(result2?.name).toEqual('nested');
});
it('simulates autocomplete typing and resolution', () => { it('simulates autocomplete typing and resolution', () => {
const subcommandName = faker.string.alpha(); const subcommandGroupName = faker.string.alpha()
const optionName = faker.word.noun(); const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [ const optionName = faker.word.noun();
{ const options: SernOptionsData[] = [
type: ApplicationCommandOptionType.SubcommandGroup, {
name: faker.string.alpha(), type: ApplicationCommandOptionType.SubcommandGroup,
description: faker.string.alpha(), name: subcommandGroupName,
options: [ description: faker.string.alpha(),
{ options: [
type: ApplicationCommandOptionType.Subcommand, {
name: subcommandName, type: ApplicationCommandOptionType.Subcommand,
description: faker.string.alpha(), name: subcommandName,
options: [ description: faker.string.alpha(),
createRandomChoice(), options: [
createRandomChoice(), createRandomChoice(),
{ createRandomChoice(),
type: ApplicationCommandOptionType.String, {
name: optionName, type: ApplicationCommandOptionType.String,
description: faker.string.alpha(), name: optionName,
autocomplete: true, description: faker.string.alpha(),
command: { autocomplete: true,
onEvent: [], command: {
execute: vi.fn(), onEvent: [],
execute: vi.fn(),
},
}, },
}, ],
], },
}, {
{ type: ApplicationCommandOptionType.Subcommand,
type: ApplicationCommandOptionType.Subcommand, name: subcommandName + 'a',
name: subcommandName + 'a', description: faker.string.alpha(),
description: faker.string.alpha(), options: [
options: [ createRandomChoice(),
createRandomChoice(), {
{ type: ApplicationCommandOptionType.String,
type: ApplicationCommandOptionType.String, name: optionName,
name: optionName, description: faker.string.alpha(),
description: faker.string.alpha(), autocomplete: true,
autocomplete: true, command: {
command: { onEvent: [],
onEvent: [], execute: vi.fn(),
execute: vi.fn(), },
}, },
}, ],
], },
}, ],
], },
}, ];
]; let accumulator = '';
let accumulator = ''; let result: unknown;
let result: unknown; const table = createLookupTable(options)
for (const char of optionName) { for (const char of optionName) {
accumulator += char; accumulator += char;
const focusedValue = {
name: accumulator,
value: faker.string.alpha(),
focused: true,
};
result = table.get(`<parent>/${subcommandGroupName}/${subcommandName}/${focusedValue.name}` );
}
expect(result).toBeTruthy();
});
})
const autocomplete = new AutocompleteInteraction(accumulator);
autocomplete.options.getSubcommand.mockReturnValue(subcommandName);
autocomplete.options.getFocused.mockReturnValue({
name: accumulator,
value: faker.string.alpha(),
focused: true,
});
result = treeSearch(autocomplete, options);
}
expect(result).toBeTruthy();
});
}); });

View File

@@ -28,3 +28,21 @@ export function createRandomModule(plugins: any[]): Processed<Module> {
execute: vi.fn(), execute: vi.fn(),
}; };
} }
export function createRandomChoice() {
return {
type: faker.number.int({ min: 1, max: 11 }),
name: faker.word.noun(),
description: faker.word.adjective(),
};
}
export function createRandomPlugins(len: number) {
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
return Array.from({ length: len }, () => ({
type: random(),
execute: () => (random() === 1 ? controller.next() : controller.stop()),
}));
}

3300
yarn.lock

File diff suppressed because it is too large Load Diff