monorepo tools first commit

This commit is contained in:
jacob
2023-08-29 17:38:30 -05:00
commit bcac9e4457
42 changed files with 6974 additions and 0 deletions

10
.editorconfig Normal file
View 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
View File

@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

10
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1 @@
yarnPath: .yarn/releases/yarn-3.6.3.cjs

1
README.md Normal file
View File

@@ -0,0 +1 @@
# tools

8
package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "tools",
"packageManager": "yarn@3.6.3",
"private": true,
"workspaces": [
"packages/*"
]
}

2
packages/builder/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist/*

View File

@@ -0,0 +1 @@
./test

View 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
View 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.

View 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
View 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 };

View 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"
}

View 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")

View 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
View 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

File diff suppressed because it is too large Load Diff

View 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"

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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!

View 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])

View 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
View 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

File diff suppressed because it is too large Load Diff

8
packages/rx/rx.iml Normal file
View 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>

View 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
View 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))));
// }
//
//

View 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)
//
// })
// })

View 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)
})
})

View 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"]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"target": "esnext"
}
}

View 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,
},
]);

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
includeSource: ["src/**/*.{js,ts}"]
},
})

0
yarn.lock Normal file
View File