mirror of
https://github.com/sern-handler/tools
synced 2026-06-06 01:16:59 +00:00
monorepo tools first commit
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,json,yml}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.yarn/*
|
||||
.yarn/releases/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
# Swap the comments on the following lines if you don't wish to use zero-installs
|
||||
# Documentation here: https://yarnpkg.com/features/zero-installs
|
||||
!.yarn/cache
|
||||
#.pnp.*
|
||||
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
||||
yarnPath: .yarn/releases/yarn-3.6.3.cjs
|
||||
8
package.json
Normal file
8
package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "tools",
|
||||
"packageManager": "yarn@3.6.3",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
2
packages/builder/.gitignore
vendored
Normal file
2
packages/builder/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist/*
|
||||
1
packages/builder/.npmignore
Normal file
1
packages/builder/.npmignore
Normal file
@@ -0,0 +1 @@
|
||||
./test
|
||||
34
packages/builder/CONTRIBUTING.md
Normal file
34
packages/builder/CONTRIBUTING.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Contributing to sern-handler/option
|
||||
|
||||
Thank you for your interest in contributing! We appreciate your help in making this project better.
|
||||
|
||||
To contribute, please follow these guidelines:
|
||||
|
||||
## Bug Reports and Feature Requests
|
||||
|
||||
If you encounter a bug or have a feature request, please submit an issue on the [GitHub repository](https://github.com/sern-handler/option/issues). Before submitting an issue, please search existing issues to avoid duplicates.
|
||||
|
||||
When submitting an issue, please provide as much detail as possible, including steps to reproduce, expected behavior, and screenshots if applicable.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
We welcome pull requests for bug fixes, improvements, and new features. To submit a pull request, please follow these steps:
|
||||
|
||||
1. Fork the repository and create your branch from `main`.
|
||||
2. Make your changes and ensure that the code follows the project's coding style and conventions.
|
||||
3. Write clear and concise commit messages.
|
||||
4. Test your changes thoroughly.
|
||||
5. Document any necessary changes in the project's documentation.
|
||||
6. Submit the pull request, providing a detailed description of the changes made.
|
||||
|
||||
## Coding Style and Conventions
|
||||
|
||||
Please adhere to the coding style and conventions used in the project. If there are specific guidelines or linting rules, they will be mentioned in the project's documentation or configuration files.
|
||||
|
||||
## License
|
||||
|
||||
By contributing to [Project Name], you agree that your contributions will be licensed under the [project's license](./LICENSE).
|
||||
|
||||
---
|
||||
|
||||
Thank you for considering contributing! We appreciate your support and look forward to your contributions.
|
||||
21
packages/builder/LICENSE
Normal file
21
packages/builder/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 sern
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
66
packages/builder/README.md
Normal file
66
packages/builder/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
# @sern/builder
|
||||
|
||||
`@sern/builder` is a TypeScript library that provides a type-safe and declarative builder to create data for the Discord API. At the moment it only creates `options for chat input command.` PRs are welcome!
|
||||
|
||||
## Installation
|
||||
|
||||
You can install `@sern/builder` using npm or yarn:
|
||||
|
||||
```bash
|
||||
npm install @sern/builder
|
||||
```
|
||||
or
|
||||
|
||||
```bash
|
||||
yarn add @sern/builder
|
||||
```
|
||||
|
||||
## Features
|
||||
- Small size: `<= 2kb`
|
||||
- Type-safe builder: Create data for the Discord API with full type checking.
|
||||
- Declarative and minimal syntax: Build data using a clean and intuitive syntax.
|
||||
- Supports all option types: String, number, attachment, integer, user, channel, and mentionable and subcommands
|
||||
- Validates data: checks names and description based on Discord Api regexes
|
||||
- 'Bottom up Builders': Each function is composable and individual,
|
||||
- Traditional builders contain an intermediary invalid state, while pure functions yield 'valid state'
|
||||
- This allows more flexible structures and substructures while being `declarative` and `less noisy`
|
||||
|
||||
## Usage
|
||||
|
||||
Here's an example of how to use `@sern/builder` to create a subcommandgroup structure for the Discord API:
|
||||
|
||||
```javascript
|
||||
import { str, name, description, NoValidator, Flags, subcommandgroup, subcommand, length, _ } from '@sern/builder';
|
||||
|
||||
const tree = subcommandgroup(
|
||||
name('group'),
|
||||
description('bunch of subcommands'),
|
||||
[
|
||||
subcommand(
|
||||
name("first"),
|
||||
description("second"),
|
||||
[
|
||||
str(
|
||||
name("choose"),
|
||||
description("pick one of the following"),
|
||||
length(_, 10),
|
||||
Flags.Required | Flags.Autocomplete),
|
||||
]
|
||||
)]
|
||||
)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to `@sern/builder` are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on the [GitHub repository](https://github.com/sern-handler/option).
|
||||
|
||||
Before contributing, please make sure to read the [Contributing Guidelines](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
Thank you for using `@sern/builder`! If you have any questions or need further assistance, please feel free to reach out.
|
||||
301
packages/builder/index.ts
Normal file
301
packages/builder/index.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import * as assert from 'node:assert';
|
||||
import { BaseOption, BranchNode, Choice, Choiceable, Description, Name, NoValidator, Validators } from './types';
|
||||
import { ApplicationCommandOptionType, ChannelType, LocalizationMap } from 'discord-api-types/v10'
|
||||
|
||||
/*
|
||||
* placeholder type for ranges
|
||||
* @example
|
||||
* ```ts
|
||||
* str(
|
||||
* name('example'),
|
||||
* description('example'),
|
||||
* length(_, 10), //no min, max length of 10
|
||||
* Flags.Required
|
||||
* )
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export const _ = NaN;
|
||||
export enum Flags {
|
||||
None = 0,
|
||||
Required = 1 << 0,
|
||||
Autocomplete = 1 << 1,
|
||||
Nsfw = 1 << 2
|
||||
}
|
||||
function mapFlags(flags: Flags): Record<string,unknown> {
|
||||
const output: Record<string,unknown> = {};
|
||||
|
||||
if (flags & Flags.Required) {
|
||||
output.required = true;
|
||||
}
|
||||
|
||||
if (flags & Flags.Autocomplete) {
|
||||
output.autocomplete = true;
|
||||
}
|
||||
|
||||
if (flags & Flags.Nsfw) {
|
||||
output.nsfw = true;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to provide localization options to options
|
||||
*/
|
||||
export function local<T extends ApplicationCommandOptionType>(
|
||||
b: BaseOption<T>,
|
||||
options : { name_localizations?: LocalizationMap|null; description_localizations?: LocalizationMap|null }
|
||||
) {
|
||||
return {
|
||||
...b,
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* declare range for number option
|
||||
*/
|
||||
export function range<T extends
|
||||
ApplicationCommandOptionType.Number
|
||||
| ApplicationCommandOptionType.Integer
|
||||
>(min?: number, max?: number): Validators[T] {
|
||||
return {
|
||||
min_value: Number.isNaN(min) ? undefined : min,
|
||||
max_value: Number.isNaN(max) ? undefined : max
|
||||
} as Validators[T];
|
||||
}
|
||||
|
||||
export function length<T extends ApplicationCommandOptionType.String>(min?: number, max?: number): Validators[T] {
|
||||
const base = {
|
||||
min_length: Number.isNaN(min) ? undefined : min,
|
||||
max_length: Number.isNaN(max) ? undefined : max
|
||||
} as Validators[T];
|
||||
|
||||
if(typeof base.min_length === 'number') {
|
||||
assert.ok(6000 >= base.min_length && base.min_length >= 0, "Invalid range: min length should be 0 <= x <= 6000" )
|
||||
}
|
||||
if(typeof base.max_length === 'number') {
|
||||
assert.ok(6000 >= base.max_length && base.max_length >= 1, "Invalid range: min length should be 1 <= x <= 6000" )
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function baseOption<T extends ApplicationCommandOptionType>(
|
||||
type: T,
|
||||
name: string,
|
||||
description: string,
|
||||
flags: Flags,
|
||||
other: Record<string, unknown> = {}
|
||||
): BaseOption<T> {
|
||||
return {
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
...mapFlags(flags),
|
||||
...other
|
||||
};
|
||||
}
|
||||
/**
|
||||
* For choices that have the same name and value
|
||||
*/
|
||||
export function identity(value: string) {
|
||||
return { name: value, value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents any option that is a choice
|
||||
* ie: String, Number, or Integer option
|
||||
*/
|
||||
export function choice<T extends Choiceable>(
|
||||
choiceable: BaseOption<T>,
|
||||
choices: Choice<T>[]
|
||||
) {
|
||||
assert.ok(!choiceable.autocomplete, "Cannot have autocomplete set to true with choices enabled");
|
||||
//@ts-ignore
|
||||
choiceable.choices = choices;
|
||||
return choiceable as BaseOption<T> & { choices: Choice<T>[] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a string option
|
||||
*/
|
||||
export function str<T extends ApplicationCommandOptionType.String>(
|
||||
name: Name, description: Description<T>,
|
||||
validators: Validators[T] = NoValidator,
|
||||
flags: Flags = Flags.None,
|
||||
) {
|
||||
return baseOption(ApplicationCommandOptionType.String, name, description, flags, validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a number option
|
||||
*/
|
||||
export function num<T extends ApplicationCommandOptionType.Number>(
|
||||
name: Name,
|
||||
description: Description<T>,
|
||||
validators: Validators[T] = NoValidator,
|
||||
flags: Flags = Flags.None,
|
||||
) {
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.Number,
|
||||
name,
|
||||
description,
|
||||
flags,
|
||||
validators
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Represents a attachment option
|
||||
*/
|
||||
export function attachment(
|
||||
name: Name,
|
||||
description: Description<ApplicationCommandOptionType.Attachment>,
|
||||
flags: Flags= Flags.None
|
||||
)
|
||||
{
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.Attachment,
|
||||
name,
|
||||
description,
|
||||
flags
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a integer option
|
||||
*/
|
||||
export function int(
|
||||
name: Name,
|
||||
description: Description<ApplicationCommandOptionType.Integer>,
|
||||
validators: Validators[ApplicationCommandOptionType.Integer] = NoValidator,
|
||||
flags: Flags= Flags.None
|
||||
|
||||
) {
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.Integer,
|
||||
name,
|
||||
description,
|
||||
flags,
|
||||
validators
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Represents a user option
|
||||
*/
|
||||
export function user(
|
||||
name: Name,
|
||||
description: Description<ApplicationCommandOptionType.User>,
|
||||
flags: Flags= Flags.None
|
||||
|
||||
) {
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.User,
|
||||
name,
|
||||
description,
|
||||
flags
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Represents a channel option
|
||||
*/
|
||||
export function channel(
|
||||
name: Name,
|
||||
description: Description<ApplicationCommandOptionType.Channel>,
|
||||
channel_types: ChannelType[] = [],
|
||||
flags: Flags= Flags.None
|
||||
|
||||
) {
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.Channel,
|
||||
name,
|
||||
description,
|
||||
flags,
|
||||
{ channel_types }
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Represents a mentionable option
|
||||
*/
|
||||
export function mentionable(
|
||||
name: Name,
|
||||
description: Description<ApplicationCommandOptionType.Mentionable>,
|
||||
flags: Flags= Flags.None
|
||||
|
||||
) {
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.Mentionable,
|
||||
name,
|
||||
description,
|
||||
flags
|
||||
);
|
||||
}
|
||||
/*
|
||||
* wrapper function validating a string by Discord command / option name regex,
|
||||
* https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
||||
*/
|
||||
export function name(v: string): Name {
|
||||
//idk if unicode flag is set yet!
|
||||
assert.match(v, /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/gu, v + " does not match a valid command name")
|
||||
return v as Name;
|
||||
}
|
||||
|
||||
/*
|
||||
* wrapper function validating a string description,
|
||||
* the length must be 0 <= x <= 100
|
||||
*/
|
||||
export function description<T extends ApplicationCommandOptionType>(args: string) {
|
||||
assert.ok(0 <= args.length && args.length <= 100)
|
||||
return args as unknown as Description<T>;
|
||||
}
|
||||
|
||||
export function subcommand(
|
||||
name: Name,
|
||||
description: Description<ApplicationCommandOptionType.Subcommand>,
|
||||
options: BaseOption<ApplicationCommandOptionType>[] = [],
|
||||
flags: Flags = Flags.None
|
||||
) {
|
||||
assert.ok(!(flags & (Flags.Autocomplete | Flags.Required)), "Cannot have autocomplete or required flag on subcommand");
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.Subcommand,
|
||||
name,
|
||||
description,
|
||||
flags,
|
||||
{ options }
|
||||
) as BranchNode<ApplicationCommandOptionType.Subcommand>
|
||||
}
|
||||
|
||||
export function subcommandgroup(
|
||||
name: Name,
|
||||
description: Description<ApplicationCommandOptionType.Subcommand>,
|
||||
options: BaseOption<ApplicationCommandOptionType.Subcommand>[],
|
||||
flags: Flags = Flags.None
|
||||
) {
|
||||
assert.ok(!(flags & (Flags.Autocomplete | Flags.Required)), "Cannot have autocomplete or required flag on subcommandgroup");
|
||||
assert.ok(options.every(t => t.type === ApplicationCommandOptionType.Subcommand))
|
||||
return baseOption(
|
||||
ApplicationCommandOptionType.SubcommandGroup,
|
||||
name,
|
||||
description,
|
||||
flags,
|
||||
{ options }
|
||||
) as BranchNode<ApplicationCommandOptionType.SubcommandGroup>;
|
||||
}
|
||||
/*
|
||||
* For sern/handler usage only- sern/handler handles autocomplete in options structures
|
||||
* For pure Discord API, enable the Autocomplete flag on the option
|
||||
*/
|
||||
export function autocomplete<T>(b: BaseOption<ApplicationCommandOptionType>, cb: (args: T) => PromiseLike<unknown> | unknown ) {
|
||||
if(!b.autocomplete) {
|
||||
b.autocomplete = true
|
||||
}
|
||||
return {
|
||||
...b,
|
||||
command: {
|
||||
onEvent: [],
|
||||
execute: cb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { Choice, NoValidator, BaseOption };
|
||||
30
packages/builder/package.json
Normal file
30
packages/builder/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@sern/builder",
|
||||
"version": "1.0.0-rc1",
|
||||
"description": "Type safe options builder for the discord api",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"scripts": {
|
||||
"bundle": "microbundle --target node",
|
||||
"watch": "microbundle watch",
|
||||
"test": "microbundle --target node && node ./test/index.test.mjs"
|
||||
},
|
||||
"exports": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"default": "./dist/index.modern.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"discord-api-types": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"microbundle": "^0.15.1",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
173
packages/builder/test/index.test.mjs
Normal file
173
packages/builder/test/index.test.mjs
Normal file
@@ -0,0 +1,173 @@
|
||||
import * as assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
str,
|
||||
name ,
|
||||
description,
|
||||
Flags,
|
||||
choice,
|
||||
identity,
|
||||
subcommand,
|
||||
subcommandgroup,
|
||||
length
|
||||
} from '../dist/index.js'
|
||||
|
||||
assert.deepEqual(
|
||||
str(
|
||||
name("option"),
|
||||
description("a string option")
|
||||
),
|
||||
{
|
||||
name: "option",
|
||||
description: "a string option",
|
||||
type: 3
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
assert.deepEqual(
|
||||
str(
|
||||
name("option"),
|
||||
description("a string option"),
|
||||
length(1, 10)
|
||||
),
|
||||
{
|
||||
name: "option",
|
||||
description: "a string option",
|
||||
type: 3,
|
||||
max_length: 10,
|
||||
min_length: 1
|
||||
}
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
str(
|
||||
name("option"),
|
||||
description("option"),
|
||||
{},
|
||||
Flags.Nsfw | Flags.Required
|
||||
),
|
||||
{
|
||||
type: 3,
|
||||
name: "option",
|
||||
description: "option",
|
||||
nsfw: true,
|
||||
required: true
|
||||
}
|
||||
)
|
||||
assert.deepEqual(
|
||||
choice(
|
||||
str(
|
||||
name("option"),
|
||||
description("option"),
|
||||
{},
|
||||
Flags.Nsfw | Flags.Required
|
||||
),
|
||||
[identity("option1"), identity("option2")]
|
||||
),
|
||||
{
|
||||
type: 3,
|
||||
name: "option",
|
||||
description: "option",
|
||||
nsfw: true,
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: "option1", value: "option1" },
|
||||
{ name: "option2", value: "option2" }
|
||||
]
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
choice(
|
||||
str(
|
||||
name("option"),
|
||||
description("option"),
|
||||
{},
|
||||
Flags.Nsfw | Flags.Required
|
||||
),
|
||||
[identity("option1"), identity("option2")]
|
||||
),
|
||||
{
|
||||
type: 3,
|
||||
name: "option",
|
||||
description: "option",
|
||||
nsfw: true,
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: "option1", value: "option1" },
|
||||
{ name: "option2", value: "option2" }
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => str(
|
||||
name("bad option name"),
|
||||
description("shid")
|
||||
),
|
||||
"name fails regex"
|
||||
)
|
||||
|
||||
assert.throws(
|
||||
() => str(
|
||||
name("bad option name"),
|
||||
description()
|
||||
),
|
||||
"No description"
|
||||
)
|
||||
|
||||
assert.deepStrictEqual(
|
||||
subcommand(
|
||||
name("eat"),
|
||||
description("eat cheese"),
|
||||
[
|
||||
str( name("gouda"), description("smoked gouda")),
|
||||
str( name("parmesan"), description("yummy parm"))
|
||||
],
|
||||
),
|
||||
{
|
||||
type: 1,
|
||||
name: "eat",
|
||||
description: "eat cheese",
|
||||
options: [
|
||||
{ name: "gouda", description: "smoked gouda", type: 3 },
|
||||
{ name: "parmesan", description: "yummy parm", type: 3 },
|
||||
]
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
assert.deepStrictEqual(
|
||||
subcommandgroup(
|
||||
name("eat"),
|
||||
description("eat cheese"),
|
||||
[
|
||||
subcommand(
|
||||
name("eat"),
|
||||
description("eat cheese"),
|
||||
[
|
||||
str( name("gouda"), description("smoked gouda")),
|
||||
str( name("parmesan"), description("yummy parm"))
|
||||
]
|
||||
),
|
||||
],
|
||||
),
|
||||
{
|
||||
type: 2,
|
||||
name: "eat",
|
||||
description: "eat cheese",
|
||||
options: [
|
||||
{
|
||||
name: "eat",
|
||||
description: "eat cheese",
|
||||
type: 1,
|
||||
options: [
|
||||
{ name: "gouda", description:"smoked gouda", type: 3 },
|
||||
{ name: "parmesan", description:"yummy parm", type: 3 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
)
|
||||
console.log("OK")
|
||||
109
packages/builder/tsconfig.json
Normal file
109
packages/builder/tsconfig.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ESNext", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
46
packages/builder/types.ts
Normal file
46
packages/builder/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { APIApplicationCommandOptionBase, ApplicationCommandOptionType } from "discord-api-types/v10";
|
||||
|
||||
|
||||
export const NoValidator = {};
|
||||
|
||||
export type Brand<K, T> = K & { __brand: T }
|
||||
export type Name = Brand<string, 'Must be name'>
|
||||
export type Description<_ extends ApplicationCommandOptionType> = Brand<string, 'Must be description'>
|
||||
|
||||
|
||||
export type BaseOption<T extends ApplicationCommandOptionType> = APIApplicationCommandOptionBase<T> & {
|
||||
autocomplete?: boolean;
|
||||
required?: boolean;
|
||||
nsfw?: boolean
|
||||
}
|
||||
export type BranchableTypes = ApplicationCommandOptionType.SubcommandGroup|ApplicationCommandOptionType.Subcommand;
|
||||
/*
|
||||
* For either Subcommand or SubcommandGroup types. These are the only nodes that cannot be leaf nodes of
|
||||
* options tree
|
||||
*/
|
||||
export type BranchNode<T extends BranchableTypes> = APIApplicationCommandOptionBase<T> & {
|
||||
autocomplete?: never;
|
||||
required?: never;
|
||||
}
|
||||
interface OptionTypeToPrimitive {
|
||||
[ApplicationCommandOptionType.Number]: number;
|
||||
[ApplicationCommandOptionType.String]: string;
|
||||
[ApplicationCommandOptionType.Integer]: number;
|
||||
}
|
||||
export type Choiceable =
|
||||
| ApplicationCommandOptionType.String
|
||||
| ApplicationCommandOptionType.Number
|
||||
| ApplicationCommandOptionType.Integer;
|
||||
|
||||
|
||||
export interface Choice<T extends Choiceable> {
|
||||
name: string,
|
||||
value: OptionTypeToPrimitive[T]
|
||||
}
|
||||
|
||||
export interface Validators {
|
||||
[ApplicationCommandOptionType.Number]: { min_value?: number; max_value?: number }
|
||||
[ApplicationCommandOptionType.Integer]: { min_value?: number; max_value?: number }
|
||||
[ApplicationCommandOptionType.String]: { max_length?: number; min_length?: number },
|
||||
}
|
||||
|
||||
3382
packages/builder/yarn.lock
Normal file
3382
packages/builder/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
29
packages/rx/.github/workflows/npm-publish-github-packages.yml
vendored
Normal file
29
packages/rx/.github/workflows/npm-publish-github-packages.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
||||
|
||||
name: Node.js Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
test-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 17
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
run_install: |
|
||||
- recursive: true
|
||||
args: [--strict-peer-dependencies]
|
||||
- run: pnpm install
|
||||
- run: pnpm test
|
||||
- run: pnpm build
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: "public"
|
||||
13
packages/rx/.github/workflows/release-please.yml
vendored
Normal file
13
packages/rx/.github/workflows/release-please.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: release-please
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: google-github-actions/release-please-action@v3.1.2
|
||||
with:
|
||||
release-type: node
|
||||
package-name: release-please-action
|
||||
bump-patch-for-minor-pre-major: true
|
||||
117
packages/rx/.gitignore
vendored
Normal file
117
packages/rx/.gitignore
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
8
packages/rx/.idea/.gitignore
generated
vendored
Normal file
8
packages/rx/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
7
packages/rx/.idea/discord.xml
generated
Normal file
7
packages/rx/.idea/discord.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
9
packages/rx/.idea/misc.xml
generated
Normal file
9
packages/rx/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettingsMigration">
|
||||
<option name="stateVersion" value="1" />
|
||||
</component>
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
packages/rx/.idea/modules.xml
generated
Normal file
8
packages/rx/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/rx.iml" filepath="$PROJECT_DIR$/rx.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
packages/rx/.idea/vcs.xml
generated
Normal file
6
packages/rx/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
12
packages/rx/.npmignore
Normal file
12
packages/rx/.npmignore
Normal file
@@ -0,0 +1,12 @@
|
||||
src
|
||||
build
|
||||
.editorconfig
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.travis.yml
|
||||
rollup.config.dev.js
|
||||
rollup.config.js
|
||||
yarn-error.log
|
||||
vitest.config.ts
|
||||
test/**/*
|
||||
examples
|
||||
51
packages/rx/CHANGELOG.md
Normal file
51
packages/rx/CHANGELOG.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
### [0.0.3-alpha](https://github.com/sern-handler/rx/compare/v0.0.2-alpha...v0.0.3-alpha) (2023-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add optional takeN index to toCollection ([8f6ac9f](https://github.com/sern-handler/rx/commit/8f6ac9fa20fff489eed1fcb2b01c74efad4bbee2))
|
||||
* parameterize source ([feb4776](https://github.com/sern-handler/rx/commit/feb4776d0be2c71025bd18165e097f786dfc17fb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* generics ([51aba2a](https://github.com/sern-handler/rx/commit/51aba2a444067505b9b39cd6347c24084b733ef9))
|
||||
* generics ([1d1f946](https://github.com/sern-handler/rx/commit/1d1f946cc7fbd9656acdff13f98d25de03d9ebb0))
|
||||
* update package.json ([0771676](https://github.com/sern-handler/rx/commit/07716765731be34407f889ed0d58aca36c407f0a))
|
||||
|
||||
### [0.0.2-alpha](https://github.com/sern-handler/rx/compare/v0.0.1-alpha...v0.0.2-alpha) (2023-02-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add discord.js examples ([91de33d](https://github.com/sern-handler/rx/commit/91de33dda327843caf1b7c0b77cbf42e096f8cbe))
|
||||
* build for esm only ([5006f96](https://github.com/sern-handler/rx/commit/5006f96149432fa563e4db86a5fa25cf0df751d8))
|
||||
* buttons ([d2ec056](https://github.com/sern-handler/rx/commit/d2ec056f4d1e4ab950266f693b385384c9cfc789))
|
||||
* remove pipeline option ([baecfb6](https://github.com/sern-handler/rx/commit/baecfb6bb1de624aa2897492f54788d4d3bd4402))
|
||||
* testing composable-style functions ([cd1fcf7](https://github.com/sern-handler/rx/commit/cd1fcf79825968cf1b6b75816bdc1da9add96124))
|
||||
* update dependencies and tsup ([2da5fd0](https://github.com/sern-handler/rx/commit/2da5fd0b7fc360c370bae4ba4d63144977790f13))
|
||||
|
||||
### 0.0.1-alpha (2023-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add more methods ([7647d92](https://github.com/sern-handler/rx/commit/7647d925865098a80859408731c55f55bb3661ca))
|
||||
* add type predicates for djs bindings ([769bdf6](https://github.com/sern-handler/rx/commit/769bdf65b408acbdcb82d6851947bb05e5791c3b))
|
||||
* adding scripts and some DJS bindings ([6926098](https://github.com/sern-handler/rx/commit/69260980d71a30ba5541394e420409128d4b4e9b))
|
||||
* completeOnFirst works? ([4e967ed](https://github.com/sern-handler/rx/commit/4e967ed096882f612926a94bd54305f9cf093ed8))
|
||||
* first commit ([0fc4004](https://github.com/sern-handler/rx/commit/0fc40048815078fc896fef5a618b7c732268cc32))
|
||||
* remove redundant count operator and pause completeOnFirst ([eaeee0f](https://github.com/sern-handler/rx/commit/eaeee0fb6f0e6bde4b8a565f1cc1bfb1e5c069a7))
|
||||
* update and work ([c1047a6](https://github.com/sern-handler/rx/commit/c1047a6fefb4f59f7a275db6b0d86664d5b10cdb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* weird thing where instanceof is not working ([eea0286](https://github.com/sern-handler/rx/commit/eea0286bcc684266cabdfc8e98cfe0e4f81afc22))
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* release 0.0.1-alpha ([fd85195](https://github.com/sern-handler/rx/commit/fd85195fb3f15fcb46ebe2057f3f449a741f7944))
|
||||
7
packages/rx/README.md
Normal file
7
packages/rx/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# @sern/rx
|
||||
A package extending rxjs for discord api libraries
|
||||
|
||||
this package offers a reactive, declarative solution using the wonderful [rxjs](https://github.com/ReactiveX/rxjs) to the current collectors
|
||||
and listeners in many discord bot libraries. It's still in alpha, but most operators and functions have been tested using vitest.
|
||||
|
||||
- View [examples](https://github.com/sern-handler/rx/tree/main/examples) to get started!
|
||||
18
packages/rx/examples/composable.ts
Normal file
18
packages/rx/examples/composable.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {composable, useMutableState} from "../src/index";
|
||||
import {fromEvent} from "rxjs";
|
||||
import {EventEmitter} from "events";
|
||||
|
||||
|
||||
//hypothetical EventEmitter
|
||||
declare const ee : EventEmitter
|
||||
|
||||
const [str, setData, manager] = useMutableState("root")
|
||||
const messageCreate = fromEvent(ee, 'messageCreate')
|
||||
|
||||
composable<string>((close, message) => {
|
||||
|
||||
if(message === "!ping") {
|
||||
setData(message)
|
||||
}
|
||||
|
||||
},[messageCreate])
|
||||
39
packages/rx/examples/discordjs/collectors.ts
Normal file
39
packages/rx/examples/discordjs/collectors.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {ButtonInteraction, ChatInputCommandInteraction, Collection, ComponentType} from "discord.js";
|
||||
import {filter, firstValueFrom, take} from 'rxjs'
|
||||
import {asyncTask, DJS, on, once, time} from "../../src/index.js";
|
||||
import {clientEvent, matchesCustomId} from "../../src/djs";
|
||||
|
||||
//Hypothetical interaction source
|
||||
declare const ctx : ChatInputCommandInteraction
|
||||
|
||||
// create a collector that accepts distinct interactions.
|
||||
// A timer is set to 5 seconds and it will emit one collection where its key is the interaction's id and value the interaction.
|
||||
const reactiveCollector = on<ButtonInteraction>('collect', ctx.channel.createMessageComponentCollector({ componentType: ComponentType.Button }))
|
||||
.pipe(
|
||||
DJS.distinctCustomId,
|
||||
time(5000),
|
||||
DJS.toCollection(src => [src.id, src], 4),
|
||||
)
|
||||
//Collection as promise
|
||||
const promisedCollection = firstValueFrom(reactiveCollector, { defaultValue : new Collection() })
|
||||
|
||||
//Collection subscription
|
||||
const subscriptionCollection = reactiveCollector.subscribe({
|
||||
next: (collection) => console.log(collection),
|
||||
error: (err) => console.log(err),
|
||||
complete: () => console.log("The collector has finished") //synonymous to the 'end' event for basic discord.js collectors!
|
||||
})
|
||||
|
||||
//Creates a base button handler that matches the given customId
|
||||
const buttonHandler = clientEvent(ctx.client, 'interactionCreate')
|
||||
.pipe(
|
||||
filter(DJS.isButton), matchesCustomId("i-miss-her"),
|
||||
)
|
||||
|
||||
//From the buttonHandler, defer the update and set a timeout of 60_000. Then it executes once, finally closing the subscription
|
||||
buttonHandler.pipe(
|
||||
asyncTask( b => b.deferUpdate()),
|
||||
time(60_000 ), // 1 minute
|
||||
once((b) => b.editReply(`You clicked a button once`))
|
||||
).subscribe()
|
||||
|
||||
50
packages/rx/package.json
Normal file
50
packages/rx/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@sern/rx",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs"
|
||||
},
|
||||
"./djs": {
|
||||
"import": "./dist/djs/index.mjs"
|
||||
}
|
||||
},
|
||||
"version": "0.0.3-alpha",
|
||||
"description": "rxjs operators and utils for discord bot projects",
|
||||
"packageManager": "pnpm@7.24.3",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"tdd": "vitest --config ./vitest.config.ts",
|
||||
"test": "vitest run --config ./vitest.config.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "jacoobes",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.15",
|
||||
"discord.js": "^14.8.1",
|
||||
"rxjs": "7.8.1",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "5.0.4",
|
||||
"vitest": "^0.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"discord.js": ">= 14.8.0"
|
||||
},
|
||||
"node": ">=17.0",
|
||||
"type": "module",
|
||||
"types": "./dist\\index.d.ts",
|
||||
"directories": {
|
||||
"example": "examples",
|
||||
"test": "test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sern-handler/rx.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/sern-handler/rx/issues"
|
||||
},
|
||||
"homepage": "https://github.com/sern-handler/rx#readme"
|
||||
}
|
||||
1754
packages/rx/pnpm-lock.yaml
generated
Normal file
1754
packages/rx/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
packages/rx/rx.iml
Normal file
8
packages/rx/rx.iml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
95
packages/rx/src/djs/index.ts
Normal file
95
packages/rx/src/djs/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type {
|
||||
AnySelectMenuInteraction,
|
||||
ButtonInteraction, ChannelSelectMenuInteraction, ClientEvents,
|
||||
MentionableSelectMenuInteraction,
|
||||
ModalSubmitInteraction,
|
||||
RoleSelectMenuInteraction,
|
||||
StringSelectMenuInteraction,
|
||||
UserSelectMenuInteraction,
|
||||
} from 'discord.js'
|
||||
import { Collection } from "discord.js";
|
||||
import {
|
||||
distinctUntilKeyChanged,
|
||||
filter,
|
||||
MonoTypeOperatorFunction,
|
||||
Observable,
|
||||
OperatorFunction,
|
||||
pipe,
|
||||
reduce,
|
||||
take
|
||||
} from "rxjs";
|
||||
import type {Client} from "discord.js";
|
||||
import {CustomId, on} from "../index.js";
|
||||
|
||||
/**
|
||||
* discord.js binding to ensure an event is {@link ButtonInteraction}
|
||||
* @param e
|
||||
*/
|
||||
export function isButton(e: unknown): e is ButtonInteraction {
|
||||
return (e as ButtonInteraction)?.isButton?.() ?? false
|
||||
}
|
||||
|
||||
export function matchesCustomId<T extends CustomId>(cmpt: string) : MonoTypeOperatorFunction<T> {
|
||||
return filter(i => i.customId == cmpt)
|
||||
}
|
||||
|
||||
/**
|
||||
* discord.js binding to ensure an event is {@link AnySelectMenuInteraction}
|
||||
* Reminder that AnySelectMenuInteraction will be renamed to SelectMenuInteraction in the next major
|
||||
* @param e
|
||||
*/
|
||||
export function isSelectMenu(e: unknown): e is AnySelectMenuInteraction {
|
||||
return (e as AnySelectMenuInteraction)?.isAnySelectMenu?.() ?? false
|
||||
}
|
||||
|
||||
export function isRoleSelectMenu(e: unknown): e is RoleSelectMenuInteraction {
|
||||
return (e as RoleSelectMenuInteraction)?.isRoleSelectMenu?.() ?? false
|
||||
}
|
||||
|
||||
export function isStringSelectMenu(e: unknown): e is StringSelectMenuInteraction {
|
||||
return (e as StringSelectMenuInteraction)?.isStringSelectMenu?.() ?? false
|
||||
}
|
||||
|
||||
export function isChannelSelectMenu(e: unknown): e is ChannelSelectMenuInteraction {
|
||||
return (e as ChannelSelectMenuInteraction)?.isChannelSelectMenu?.() ?? false
|
||||
}
|
||||
|
||||
export function isUserSelectMenu(e: unknown): e is UserSelectMenuInteraction {
|
||||
return (e as UserSelectMenuInteraction)?.isUserSelectMenu?.() ?? false;
|
||||
}
|
||||
|
||||
export function isMentionableSelectMenu(e: unknown) : e is MentionableSelectMenuInteraction {
|
||||
return (e as MentionableSelectMenuInteraction)?.isMentionableSelectMenu?.() ?? false;
|
||||
}
|
||||
|
||||
export function isModal(e: unknown) : e is ModalSubmitInteraction {
|
||||
return (e as ModalSubmitInteraction)?.isModalSubmit?.() ?? false;
|
||||
|
||||
}
|
||||
/**
|
||||
* emits source stream only if the custom id is unique
|
||||
*/
|
||||
export const distinctCustomId = <T extends { customId: string }> (src$: Observable<T>) => src$.pipe(distinctUntilKeyChanged('customId'));
|
||||
|
||||
//Collects all source emissions and emits them as a discord.js Collection when the source completes.
|
||||
export function toCollection<Source, Key, Value>(
|
||||
transform: (src : Source) => [Key, Value],
|
||||
takeN?: number
|
||||
): OperatorFunction<Source, Collection<Key,Value>> {
|
||||
return pipe(
|
||||
takeN ? take(takeN) : pipe(),
|
||||
reduce((coll, src) => {
|
||||
return coll.set(...transform(src));
|
||||
}, new Collection<Key, Value>())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a typed client event observable
|
||||
* @param c
|
||||
* @param key
|
||||
*/
|
||||
export function clientEvent<T extends keyof ClientEvents>(c : Client, key : T) {
|
||||
return on(key, c) as Observable<ClientEvents[T]>
|
||||
}
|
||||
196
packages/rx/src/index.ts
Normal file
196
packages/rx/src/index.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type {EventEmitter} from "events";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
defer,
|
||||
fromEvent,
|
||||
mergeMap,
|
||||
type MonoTypeOperatorFunction,
|
||||
Observable,
|
||||
Subject, switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
timer,
|
||||
concatMap,
|
||||
pipe,
|
||||
catchError
|
||||
} from "rxjs";
|
||||
|
||||
export * as DJS from './djs/index.js'
|
||||
|
||||
export interface CustomId {
|
||||
customId: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an observable using fromEvent & casts it to Observable<O>
|
||||
* @param name
|
||||
* @param e
|
||||
*/
|
||||
export function on<O>(name: string, e: EventEmitter): Observable<O> {
|
||||
return fromEvent(e, name) as Observable<O>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do some task that requires asynchronous await API and reedits the source
|
||||
* @param cb
|
||||
*/
|
||||
export function asyncTask<Event>(cb: (e: Event) => Promise<unknown>) {
|
||||
return (src$: Observable<Event>) => src$.pipe(switchMap(cb), mergeMap(() => src$))
|
||||
}
|
||||
|
||||
/**
|
||||
* the operator function takeUntil with a timer. Completes the source stream after time has been reached
|
||||
* @param time a Date object or number (milliseconds)
|
||||
*/
|
||||
export function time<Event>(time: Date | number) {
|
||||
return takeUntil(timer(time)) as MonoTypeOperatorFunction<Event>
|
||||
}
|
||||
|
||||
/**
|
||||
* @param action
|
||||
* Responds to one interaction and calls { action }.
|
||||
* Unsubscribes and closes subscription afterwards
|
||||
*/
|
||||
export function once<Event>(action: (e: Event) => unknown) {
|
||||
return (source$: Observable<Event>) =>
|
||||
defer(() => {
|
||||
let isFirst = true;
|
||||
return source$.pipe(
|
||||
tap(v => {
|
||||
if (isFirst) {
|
||||
action(v);
|
||||
isFirst = false;
|
||||
}
|
||||
}),
|
||||
take(1)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @Experimental - This api may be moved to another package, deleted, or anything. consider it for playing around only
|
||||
* The scope which a composable function acts on.
|
||||
*/
|
||||
class ComposableScope<Source> {
|
||||
private symbol = Symbol("nothing");
|
||||
|
||||
constructor(
|
||||
private updater: Subject<Source>,
|
||||
private listeners: (BehaviorSubject<unknown> | Observable<unknown>)[]
|
||||
) {}
|
||||
|
||||
private executeIfUpdaterNotClosed(action: () => any) {
|
||||
if (!this.updater.closed) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
listen(recomposable: (close: () => void, source: Source) => unknown) {
|
||||
const updaterFinalizer = () => this.updater.complete();
|
||||
this.updater
|
||||
.subscribe({
|
||||
next: data => recomposable(updaterFinalizer, data),
|
||||
complete: () => {
|
||||
this.listeners.forEach(s => {
|
||||
if (s instanceof BehaviorSubject) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
})
|
||||
},
|
||||
error: () => {
|
||||
this.listeners.forEach(s => {
|
||||
if (s instanceof BehaviorSubject) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (this.listeners.length === 0) {
|
||||
this.updater.complete()
|
||||
} else {
|
||||
for (const subject of this.listeners) {
|
||||
subject.subscribe((data) => {
|
||||
if (subject instanceof Observable) {
|
||||
this.executeIfUpdaterNotClosed(() => {
|
||||
this.updater.next(data as Source)
|
||||
})
|
||||
} else {
|
||||
this.executeIfUpdaterNotClosed(() => {
|
||||
this.updater.next(this.symbol as Source)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Experimental - This api may be moved to another package, deleted, or anything. consider it for playing around only
|
||||
* @param seed - The beginning value. This works similarly to react useState or compose remember mutableStateOf
|
||||
*/
|
||||
export function useMutableState<T>(seed: T) {
|
||||
const stateManager = new BehaviorSubject(seed)
|
||||
return [
|
||||
() => stateManager.getValue(),
|
||||
(vl: T) => stateManager.next(vl),
|
||||
stateManager,
|
||||
] as const
|
||||
}
|
||||
|
||||
export type Listeners = (BehaviorSubject<unknown> | Observable<unknown>)[]
|
||||
|
||||
/**
|
||||
* @Experimental - This api may be moved to another package, deleted, or anything. consider it for playing around only
|
||||
* Creates a scope that can executed as many times as needed, provided it is listening to data.
|
||||
* @param scope - Executes callback until
|
||||
* @param listeners
|
||||
*/
|
||||
export function composable<Source>(
|
||||
scope: (close: () => void, source: Source) => unknown,
|
||||
listeners: Listeners
|
||||
) {
|
||||
const composableScope = new ComposableScope<Source>(new Subject(), listeners);
|
||||
composableScope.listen(scope)
|
||||
}
|
||||
|
||||
|
||||
export function filterMap<I, O>(cb: (i: I) => Observable<O> | Observable<never>) {
|
||||
return concatMap(cb)
|
||||
}
|
||||
type StreamResult =
|
||||
| { crash: true, error: Error }
|
||||
| { crash: false, error?: never }
|
||||
|
||||
export function failableStream<I, O(
|
||||
pipeline: typeof pipe,
|
||||
ehandler: (e: any) => StreamResult
|
||||
) {
|
||||
|
||||
return pipe(
|
||||
pipeline,
|
||||
catchError((err, caught) => {
|
||||
const { crash, error } = ehandler(err)
|
||||
if(crash) {
|
||||
throw error
|
||||
} else {
|
||||
return caught;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// *
|
||||
// * @param notifiers Should be terminal operators. Meaning, these should have some logic to close its subscription
|
||||
// */
|
||||
// export function completeOnFirst<Event>(notifiers: MonoTypeOperatorFunction<Event>[]) {
|
||||
// return (src: Observable<Event>) =>
|
||||
// src.pipe(raceWith(notifiers.map(notif => src.pipe(notif))));
|
||||
// }
|
||||
//
|
||||
//
|
||||
100
packages/rx/test/common.test.ts
Normal file
100
packages/rx/test/common.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
|
||||
import {composable, once, time, useMutableState} from '../src/index'
|
||||
import {of, from } from 'rxjs'
|
||||
describe('sern/rx common operators', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach( () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("subscription completes after 2 seconds", () => {
|
||||
const completion = vi.fn(() => void 0)
|
||||
of("x").pipe(time(5_000)).subscribe({
|
||||
complete: () => {
|
||||
completion()
|
||||
}
|
||||
})
|
||||
vi.advanceTimersByTime(2000)
|
||||
expect(completion).toHaveBeenCalled()
|
||||
});
|
||||
|
||||
it("should call once and close", () => {
|
||||
const tapOnce = vi.fn((a : number) => a)
|
||||
const sub = from([1,2,3,4,5]).pipe(once(tapOnce)).subscribe()
|
||||
|
||||
expect(tapOnce).toHaveBeenCalledOnce()
|
||||
expect(sub.closed).toBe(true)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe("composable", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
it("should close without calling unsubscribe manually", () => {
|
||||
const [data, setData, manager] = useMutableState("shiddd")
|
||||
composable((close) => {
|
||||
if(data() === "shiddd") {
|
||||
setData("pooo")
|
||||
} else close()
|
||||
}, [manager]);
|
||||
expect(manager.closed).toBe(true)
|
||||
})
|
||||
|
||||
it("should invoke size of string's length", () => {
|
||||
let invoke = 0
|
||||
const theString = "shiddd"
|
||||
const [data, setData, manager] = useMutableState(theString)
|
||||
composable((close) => {
|
||||
const str = data()
|
||||
if(str.length != 0) {
|
||||
invoke++
|
||||
setData(str.slice(0, str.length-1))
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}, [manager]);
|
||||
expect(manager.closed).toBe(true)
|
||||
expect(invoke).toBe(theString.length)
|
||||
})
|
||||
|
||||
it("should act as an Observable stream", () => {
|
||||
let invoke = 0
|
||||
composable(() => {
|
||||
invoke++
|
||||
}, [from([1,2,3,4,5])])
|
||||
expect(invoke).toBe(5)
|
||||
|
||||
})
|
||||
|
||||
it("should concat a string based on event", () => {
|
||||
|
||||
const [data, setData] = useMutableState("")
|
||||
composable((_, source) => {
|
||||
setData(data() + source)
|
||||
}, [from([1,2,3,4,5])])
|
||||
expect(data()).toBe("12345")
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// describe("completeFirst", () => {
|
||||
//
|
||||
// it("should invoke twice", () => {
|
||||
// let invoke = 0
|
||||
// from([1,2,3,4,5]).pipe(
|
||||
// completeOnFirst([take(2)])
|
||||
// ).subscribe((s) => {
|
||||
// console.log(s)
|
||||
// invoke++
|
||||
// })
|
||||
//
|
||||
// expect(invoke).toBe(2)
|
||||
//
|
||||
// })
|
||||
// })
|
||||
188
packages/rx/test/djs.test.js
Normal file
188
packages/rx/test/djs.test.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import {ButtonInteraction, ComponentType, InteractionType, ModalSubmitInteraction } from 'discord.js'
|
||||
import { filter, from, of } from 'rxjs'
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
||||
import { DJS } from '../src/index'
|
||||
import {clientEvent, distinctCustomId, matchesCustomId, toCollection} from "../src/djs";
|
||||
import {EventEmitter} from "events";
|
||||
import {fail} from "assert";
|
||||
import exp from "constants";
|
||||
|
||||
vi.mock('discord.js', () => {
|
||||
|
||||
const Collection = Map
|
||||
const ModalSubmitInteraction = class {
|
||||
customId
|
||||
type = 5
|
||||
isModalSubmit = vi.fn()
|
||||
constructor(customId) {
|
||||
this.customId = customId
|
||||
}
|
||||
}
|
||||
const ButtonInteraction = class {
|
||||
customId
|
||||
type = 3
|
||||
componentType = 2
|
||||
isButton = vi.fn()
|
||||
constructor(customId) {
|
||||
this.customId = customId
|
||||
}
|
||||
}
|
||||
return {
|
||||
Collection,
|
||||
ComponentType: {
|
||||
Button: 2
|
||||
},
|
||||
InteractionType : {
|
||||
MessageComponent : 3,
|
||||
ModalSubmit : 5
|
||||
},
|
||||
ModalSubmitInteraction,
|
||||
ButtonInteraction
|
||||
};
|
||||
})
|
||||
|
||||
describe("test isButton ", () => {
|
||||
let btnInteraction
|
||||
beforeEach( () => {
|
||||
btnInteraction = new ButtonInteraction("hello")
|
||||
})
|
||||
afterEach(() => vi.clearAllMocks())
|
||||
it("should invoke isButton 0 times", () => {
|
||||
let invoked = 0;
|
||||
from("not a discord button interaction").pipe(
|
||||
filter(DJS.isButton)
|
||||
).subscribe(() => invoked++)
|
||||
expect(invoked).toBe(0)
|
||||
|
||||
});
|
||||
|
||||
it("should invoke isButton once", () => {
|
||||
let invoked2 = 0;
|
||||
btnInteraction.isButton.mockReturnValue(
|
||||
btnInteraction.componentType === ComponentType.Button
|
||||
&& btnInteraction.type === InteractionType.MessageComponent
|
||||
)
|
||||
of(btnInteraction).pipe(
|
||||
filter(DJS.isButton)
|
||||
).subscribe(() => invoked2++)
|
||||
expect(invoked2).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("match customid operator", () => {
|
||||
let buttonInteraction
|
||||
|
||||
beforeEach(() => {
|
||||
buttonInteraction = new ButtonInteraction("hello-world")
|
||||
buttonInteraction.isButton.mockReturnValue(true)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
it("shouldn't invoke", () => {
|
||||
let invoked = 0
|
||||
|
||||
of(buttonInteraction).pipe(
|
||||
filter(DJS.isButton),
|
||||
matchesCustomId("pooba")
|
||||
).subscribe(() => invoked++)
|
||||
expect(invoked).toBe(0)
|
||||
|
||||
})
|
||||
it("should invoke once", () => {
|
||||
let invoked = 0
|
||||
of(buttonInteraction).pipe(
|
||||
filter(DJS.isButton),
|
||||
matchesCustomId("hello-world")
|
||||
).subscribe(() => invoked++)
|
||||
expect(invoked).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("toCollection operator", () => {
|
||||
it("should fill 10 elements", () => {
|
||||
const expectedMap = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
[5, 5],
|
||||
[6, 6],
|
||||
[7, 7],
|
||||
[8, 8],
|
||||
[9, 9],
|
||||
[10, 10]
|
||||
])
|
||||
let coll
|
||||
from([1,2,3,4,5,6,7,8,9,10]).pipe(
|
||||
toCollection(src => [src, src])
|
||||
).subscribe(collection => {
|
||||
coll = collection
|
||||
})
|
||||
expect(coll).toEqual(expectedMap)
|
||||
})
|
||||
})
|
||||
|
||||
describe("distinct custom id operator", () => {
|
||||
it("should invoke once", () => {
|
||||
let invoked = 0;
|
||||
from([new ButtonInteraction("p1"),new ButtonInteraction("p1"),new ButtonInteraction("p1") ])
|
||||
.pipe(distinctCustomId)
|
||||
.subscribe(() => invoked++)
|
||||
expect(invoked).toBe(1)
|
||||
})
|
||||
|
||||
it("should invoke twice", () => {
|
||||
let invoked = 0;
|
||||
from([new ButtonInteraction("p1"),new ButtonInteraction("p1"),new ButtonInteraction("p2") ])
|
||||
.pipe(distinctCustomId)
|
||||
.subscribe(() => invoked++)
|
||||
expect(invoked).toBe(2)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe("isModal predicate", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
it("should invoke once", () => {
|
||||
let invoked = 0
|
||||
const m = new ModalSubmitInteraction("p1")
|
||||
m.isModalSubmit.mockReturnValue(m.type === InteractionType.ModalSubmit)
|
||||
const b = new ButtonInteraction("p2")
|
||||
b.isButton.mockReturnValue(b.type === InteractionType.MessageComponent && b.componentType === ComponentType.Button)
|
||||
from([m, b])
|
||||
.pipe(filter(DJS.isModal))
|
||||
.subscribe(() => invoked++)
|
||||
expect(invoked).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe("clientEvent observable generator", () => {
|
||||
let c
|
||||
let cEvent
|
||||
let btnClickFromClientEvent
|
||||
beforeEach(() => {
|
||||
c = new EventEmitter()
|
||||
cEvent = clientEvent(c, 'interactionCreate').subscribe((b) =>btnClickFromClientEvent = b )
|
||||
})
|
||||
afterEach( () => {
|
||||
vi.clearAllMocks()
|
||||
cEvent.unsubscribe()
|
||||
})
|
||||
|
||||
it("should yield ButtonInteraction ", () => {
|
||||
const btnClick = new ButtonInteraction("p1")
|
||||
c.emit('interactionCreate', btnClick)
|
||||
expect(btnClick).toBe(btnClickFromClientEvent)
|
||||
})
|
||||
|
||||
it("should not emit correctly", () => {
|
||||
const btnClick = new ButtonInteraction("p1")
|
||||
const modalSubmit = new ModalSubmitInteraction("p1")
|
||||
c.emit('interactionCreate', modalSubmit)
|
||||
expect(btnClickFromClientEvent).not.toBe(btnClick)
|
||||
})
|
||||
})
|
||||
18
packages/rx/tsconfig-base.json
Normal file
18
packages/rx/tsconfig-base.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"experimentalDecorators": true,
|
||||
"strictNullChecks": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src"]
|
||||
}
|
||||
8
packages/rx/tsconfig-esm.json
Normal file
8
packages/rx/tsconfig-esm.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"outDir": "dist/esm",
|
||||
"target": "esnext"
|
||||
}
|
||||
}
|
||||
25
packages/rx/tsup.config.js
Normal file
25
packages/rx/tsup.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
const shared = {
|
||||
entry: ['src/index.ts', 'src/djs/index.ts'],
|
||||
external: ['discord.js', 'rxjs'],
|
||||
platform: 'node',
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
};
|
||||
export default defineConfig([
|
||||
{
|
||||
format: 'esm',
|
||||
target: 'node17',
|
||||
tsconfig: './tsconfig-esm.json',
|
||||
dts: true,
|
||||
outDir: './dist',
|
||||
external: ['discord.js'],
|
||||
treeshake: true,
|
||||
outExtension() {
|
||||
return {
|
||||
js: '.mjs',
|
||||
};
|
||||
},
|
||||
...shared,
|
||||
},
|
||||
]);
|
||||
9
packages/rx/vitest.config.ts
Normal file
9
packages/rx/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
includeSource: ["src/**/*.{js,ts}"]
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user