├── .github
├── FUNDING.yml
└── workflows
│ └── main.yaml
├── .gitignore
├── .husky
└── commit-msg
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE.md
├── README.md
├── apps
└── docs
│ ├── .gitignore
│ ├── .prettierignore
│ ├── .prettierrc
│ ├── .vscode
│ ├── extensions.json
│ └── settings.json
│ ├── README.md
│ ├── app
│ ├── (home)
│ │ ├── builders.tsx
│ │ ├── card.tsx
│ │ ├── codes.ts
│ │ ├── hero.tsx
│ │ ├── highlights.tsx
│ │ ├── horizontal-card.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── separator.tsx
│ ├── api
│ │ ├── og
│ │ │ ├── Geist-Bold.woff
│ │ │ └── route.tsx
│ │ └── search
│ │ │ └── route.ts
│ ├── docs
│ │ ├── [[...slug]]
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── global.css
│ ├── layout.config.tsx
│ ├── layout.tsx
│ ├── sitemap.ts
│ └── source.ts
│ ├── components
│ ├── code-block.tsx
│ ├── copy-install.tsx
│ ├── grid-pattern.tsx
│ ├── icon.tsx
│ ├── logo.tsx
│ ├── meteors.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── icon.tsx
│ │ └── links.tsx
│ ├── content
│ └── docs
│ │ ├── builders
│ │ ├── autocomplete.mdx
│ │ ├── button.mdx
│ │ ├── context-menu.mdx
│ │ ├── group.mdx
│ │ ├── modal.mdx
│ │ ├── protector.mdx
│ │ ├── select-menu.mdx
│ │ ├── signal.mdx
│ │ └── slash.mdx
│ │ ├── getting-started.mdx
│ │ ├── guides
│ │ ├── implementing-cooldowns.mdx
│ │ ├── interactions-handling.mdx
│ │ ├── load-modules.mdx
│ │ ├── meta.json
│ │ ├── middlewares
│ │ │ ├── admin-only.mdx
│ │ │ ├── index.mdx
│ │ │ ├── owner-only.mdx
│ │ │ ├── verify-member-permissions.mdx
│ │ │ └── verify-member-roles.mdx
│ │ ├── registering-commands
│ │ │ ├── dynamic.mdx
│ │ │ ├── global.mdx
│ │ │ ├── guilds.mdx
│ │ │ └── index.mdx
│ │ └── working-with-signals.mdx
│ │ ├── index.mdx
│ │ ├── meta.json
│ │ ├── mutators
│ │ ├── config.mdx
│ │ ├── execute.mdx
│ │ └── protect.mdx
│ │ └── props.ts
│ ├── icons
│ ├── buymeacoffee.tsx
│ ├── cross.tsx
│ ├── discord.tsx
│ ├── discordjs.tsx
│ ├── github.tsx
│ ├── index.ts
│ ├── ko-fi.tsx
│ └── npm.tsx
│ ├── middleware.ts
│ ├── next.config.mjs
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ ├── banner.png
│ ├── icon.svg
│ ├── robots.txt
│ └── simple-banner.png
│ ├── scripts
│ └── generate-docs.mts
│ ├── source.config.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── utils
│ ├── cn.ts
│ ├── mdx-components.tsx
│ └── metadata.ts
├── package.json
├── packages
├── create-sunar
│ ├── .gitignore
│ ├── .vscode
│ │ └── settings.json
│ ├── LICENSE.md
│ ├── README.md
│ ├── biome.json
│ ├── configs
│ │ ├── static-biome.json
│ │ ├── static-eslint-js.cjs
│ │ ├── static-eslint-ts.cjs
│ │ ├── static-prettier.json
│ │ └── static-tsup.ts
│ ├── package.json
│ ├── src
│ │ ├── helpers
│ │ │ ├── copy-config.ts
│ │ │ ├── copy-template.ts
│ │ │ ├── get-dependencies.ts
│ │ │ ├── get-features.ts
│ │ │ ├── get-scripts.ts
│ │ │ ├── is-empty-dir.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── node-version.ts
│ │ │ ├── setup-features.ts
│ │ │ └── validate-name.ts
│ │ ├── index.ts
│ │ ├── setup.ts
│ │ ├── types.ts
│ │ └── utils
│ │ │ ├── constants.ts
│ │ │ ├── dependencies.ts
│ │ │ ├── features.ts
│ │ │ ├── scripts.ts
│ │ │ └── templates.ts
│ ├── templates
│ │ ├── javascript
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ │ ├── commands
│ │ │ │ └── avatar.js
│ │ │ │ ├── index.js
│ │ │ │ └── signals
│ │ │ │ ├── interaction-create.js
│ │ │ │ └── ready.js
│ │ └── typescript
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ ├── commands
│ │ │ │ └── avatar.ts
│ │ │ ├── index.ts
│ │ │ └── signals
│ │ │ │ ├── interaction-create.ts
│ │ │ │ └── ready.ts
│ │ │ └── tsconfig.json
│ ├── tsconfig.json
│ └── tsup.config.ts
└── sunar
│ ├── .gitignore
│ ├── .vscode
│ └── settings.json
│ ├── LICENSE.md
│ ├── README.md
│ ├── biome.json
│ ├── commitlint.config.ts
│ ├── package.json
│ ├── src
│ ├── builders
│ │ ├── autocomplete.ts
│ │ ├── button.ts
│ │ ├── contextMenu.ts
│ │ ├── group.ts
│ │ ├── index.ts
│ │ ├── modal.ts
│ │ ├── protector.ts
│ │ ├── selectMenu.ts
│ │ ├── signal.ts
│ │ └── slash.ts
│ ├── client.ts
│ ├── handlers
│ │ ├── autocomplete
│ │ │ ├── autocomplete.ts
│ │ │ └── index.ts
│ │ ├── button
│ │ │ ├── button.ts
│ │ │ └── index.ts
│ │ ├── contextMenu
│ │ │ ├── contextMenu.ts
│ │ │ └── index.ts
│ │ ├── cooldown
│ │ │ ├── cooldown.ts
│ │ │ └── index.ts
│ │ ├── group
│ │ │ ├── group.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── interaction
│ │ │ ├── index.ts
│ │ │ └── interaction.ts
│ │ ├── modal
│ │ │ ├── index.ts
│ │ │ └── modal.ts
│ │ ├── protectors
│ │ │ ├── index.ts
│ │ │ └── protectors.ts
│ │ ├── selectMenu
│ │ │ ├── index.ts
│ │ │ └── selectMenu.ts
│ │ ├── signals
│ │ │ ├── index.ts
│ │ │ └── signals.ts
│ │ └── slash
│ │ │ ├── index.ts
│ │ │ └── slash.ts
│ ├── index.ts
│ ├── modules
│ │ ├── dirname
│ │ │ ├── dirname.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── isESM
│ │ │ ├── index.ts
│ │ │ └── isESM.ts
│ │ ├── load
│ │ │ ├── index.ts
│ │ │ ├── load.spec.ts
│ │ │ └── load.ts
│ │ ├── resolve
│ │ │ ├── index.ts
│ │ │ └── resolve.ts
│ │ └── store
│ │ │ ├── index.ts
│ │ │ └── store.ts
│ ├── mutators
│ │ ├── config
│ │ │ ├── config.spec.ts
│ │ │ ├── config.ts
│ │ │ └── index.ts
│ │ ├── execute
│ │ │ ├── execute.spec.ts
│ │ │ ├── execute.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ └── protect
│ │ │ ├── index.ts
│ │ │ ├── protect.spec.ts
│ │ │ └── protect.ts
│ ├── registry
│ │ ├── dynamic
│ │ │ ├── dynamic.ts
│ │ │ └── index.ts
│ │ ├── global
│ │ │ ├── global.ts
│ │ │ └── index.ts
│ │ ├── guild
│ │ │ ├── guild.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── stores
│ │ ├── collections.ts
│ │ ├── context.ts
│ │ └── index.ts
│ ├── symbols.ts
│ ├── types
│ │ ├── builder.ts
│ │ ├── config.ts
│ │ ├── cooldown.ts
│ │ ├── index.ts
│ │ ├── signals.ts
│ │ └── utils.ts
│ ├── utils
│ │ ├── constants.ts
│ │ ├── enums.ts
│ │ ├── getApplicationCommands
│ │ │ ├── getApplicationCommands.ts
│ │ │ └── index.ts
│ │ ├── getGroupStoreKey
│ │ │ ├── getGroupStoreKey.ts
│ │ │ └── index.ts
│ │ ├── getSunarApplicationCommands
│ │ │ ├── getSunarApplicationCommands.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── isAutocompleteBuilder
│ │ │ ├── index.ts
│ │ │ ├── isAutocompleteBuilder.spec.ts
│ │ │ └── isAutocompleteBuilder.ts
│ │ ├── isBuilder
│ │ │ ├── index.ts
│ │ │ └── isBuilder.ts
│ │ ├── isButtonBuilder
│ │ │ ├── index.ts
│ │ │ ├── isButtonBuilder.spec.ts
│ │ │ └── isButtonBuilder.ts
│ │ ├── isContextMenuBuilder
│ │ │ ├── index.ts
│ │ │ ├── isContextMenuBuilder.spec.ts
│ │ │ └── isContextMenuBuilder.ts
│ │ ├── isGroupBuilder
│ │ │ ├── index.ts
│ │ │ ├── isGroupBuilder.spec.ts
│ │ │ └── isGroupBuilder.ts
│ │ ├── isModalBuilder
│ │ │ ├── index.ts
│ │ │ ├── isModalBuilder.spec.ts
│ │ │ └── isModalBuilder.ts
│ │ ├── isObject
│ │ │ ├── index.ts
│ │ │ ├── isObject.spec.ts
│ │ │ └── isObject.ts
│ │ ├── isRegex
│ │ │ ├── index.ts
│ │ │ ├── isRegex.spec.ts
│ │ │ └── isRegex.ts
│ │ ├── isSelectMenuBuilder
│ │ │ ├── index.ts
│ │ │ └── isSelectMenuBuilder.ts
│ │ ├── isSignalBuilder
│ │ │ ├── index.ts
│ │ │ ├── isSignalBuilder.spec.ts
│ │ │ └── isSignalBuilder.ts
│ │ ├── isSlashBuilder
│ │ │ ├── index.ts
│ │ │ ├── isSlashBuilder.spec.ts
│ │ │ └── isSlashBuilder.ts
│ │ └── resolveCooldown
│ │ │ ├── index.ts
│ │ │ ├── resolveCooldown.spec.ts
│ │ │ └── resolveCooldown.ts
│ └── vitest
│ │ ├── commands
│ │ ├── _ignored.ts
│ │ └── ping.ts
│ │ └── signals
│ │ └── ready.ts
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: 4doist
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: 4doist
14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
15 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - "**"
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: pnpm/action-setup@v4
13 |
14 | - run: pnpm install
15 | - run: pnpm run lint
16 | - run: pnpm run build
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Packages
2 | node_modules
3 |
4 | # Log files
5 | logs
6 | *.log
7 | npm-debug.log*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 |
14 | # Env
15 | .env
16 |
17 | # Dist
18 | dist
19 | dist-docs
20 |
21 | # Miscellaneous
22 | .tmp
23 | .vscode/*
24 | !.vscode/extensions.json
25 | !.vscode/settings.json
26 | .idea
27 | .DS_Store
28 | .turbo
29 | tsconfig.tsbuildinfo
30 | coverage
31 | out
32 | package.tgz
33 | tsup.config.bundled*
34 | vitest.config.ts.timestamp*
35 |
36 | # Deno
37 | deno.lock
38 |
39 | # Bun
40 | bun.lockb
41 |
42 | # yarn
43 | .pnp.*
44 | .yarn/*
45 | !.yarn/patches
46 | !.yarn/plugins
47 | !.yarn/releases
48 | !.yarn/sdks
49 | !.yarn/versions
50 |
51 | # Cache
52 | .prettiercache
53 | .eslintcache
54 | .vercel
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | pnpm exec commitlint --edit $1
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "biomejs.biome",
4 | "bradlc.vscode-tailwindcss",
5 | "esbenp.prettier-vscode"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "biomejs.biome",
3 | "typescript.tsdk": "node_modules\\typescript\\lib",
4 |
5 | "conventionalCommits.scopes": ["readme", "vscode", "jsdoc", "biome"],
6 | "cSpell.words": [
7 | "autocompletes",
8 | "biomejs",
9 | "commitlint",
10 | "cooldowns",
11 | "Protectable",
12 | "Repliable",
13 | "sunar",
14 | "treeshake"
15 | ],
16 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sunar JavaScript Community
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | # About
7 | Sunar emerges as a finely-tuned [discord.js](https://discord.js.org) framework, meticulously engineered to prioritize ease of use and efficiency.
8 |
9 | # Credits
10 | Special thanks to [discord.js](https://discord.js.org) for their incredible library that powers Sunar, to [Valibot](https://github.com/fabian-hiller/valibot) for inspiring our code structure, and to [Fumadocs](https://fumadocs.vercel.app/) for being an excellent framework for creating documentations.
11 |
12 | # License
13 | Completely free and licensed under the [MIT license](https://github.com/sunarjs/sunar/blob/main/README.md). But if you want, you can give me a star on [GitHub](https://github.com/sunarjs/sunar).
--------------------------------------------------------------------------------
/apps/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # deps
2 | /node_modules
3 |
4 | # generated content
5 | .map.ts
6 | .contentlayer
7 |
8 | # test & build
9 | /coverage
10 | /.next/
11 | /out/
12 | /build
13 | *.tsbuildinfo
14 |
15 | # misc
16 | .DS_Store
17 | *.pem
18 | /.pnp
19 | .pnp.js
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | # others
25 | .env*.local
26 | .vercel
27 | next-env.d.ts
28 | .vercel
29 | .source
--------------------------------------------------------------------------------
/apps/docs/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | .turbo
4 | content/docs
--------------------------------------------------------------------------------
/apps/docs/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "endOfLine": "auto",
5 | "semi": true,
6 | "tabWidth": 2,
7 | "printWidth": 80,
8 | "useTabs": true,
9 | "plugins": ["prettier-plugin-tailwindcss"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/docs/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/docs/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.wordWrap": "on",
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "cSpell.words": [
5 | "docgen",
6 | "fumadocs",
7 | "jsxs",
8 | "Novatrix",
9 | "rehype",
10 | "uvcanvas"
11 | ],
12 | "typescript.tsdk": "node_modules\\typescript\\lib"
13 | }
14 |
--------------------------------------------------------------------------------
/apps/docs/README.md:
--------------------------------------------------------------------------------
1 | # Sunar documentation
2 |
3 | This is a Next.js application.
4 |
5 | Run development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | pnpm dev
11 | # or
12 | yarn dev
13 | ```
14 |
15 | Open http://localhost:3000 with your browser to see the result.
16 |
17 | ## Learn More
18 |
19 | To learn more about Next.js, take a look at the following
20 | resources:
21 |
22 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
23 | features and API.
24 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
25 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/codes.ts:
--------------------------------------------------------------------------------
1 | export const slashCommandCode = `import { Slash, execute } from 'sunar';
2 |
3 | const slash = new Slash({
4 | name: 'ping',
5 | description: 'Ping command',
6 | });
7 |
8 | execute(slash, (interaction) => {
9 | interaction.reply({ content: 'Pong!' });
10 | });
11 |
12 | export { slash };`;
13 |
14 | export const contextMenuCode = `import { ContextMenu, execute } from 'sunar';
15 |
16 | const contextMenu = new ContextMenu({
17 | name: 'Ping',
18 | type: 2,
19 | });
20 |
21 | execute(contextMenu, (interaction) => {
22 | interaction.reply({ content: 'Pong!' });
23 | });
24 |
25 | export { contextMenu };`;
26 |
27 | export const signalCode = `import { Signal, execute } from 'sunar';
28 |
29 | const signal = new Signal('ready', { once: true });
30 |
31 | execute(signal, () => {
32 | console.log('Logged in!');
33 | });
34 |
35 | export { signal };`;
36 |
37 | export const buttonCode = `import { Button, execute } from 'sunar';
38 |
39 | const button = new Button({ id: 'my-button' });
40 |
41 | execute(button, (interaction) => {
42 | interaction.reply({ content: 'Pong!' });
43 | });
44 |
45 | export { button };`;
46 |
47 | export const modalCode = `import { Modal, execute } from 'sunar';
48 |
49 | const modal = new Modal({ id: 'my-modal' });
50 |
51 | execute(modal, (interaction) => {
52 | interaction.reply({ content: 'Pong!' });
53 | });
54 |
55 | export { modal };`;
56 |
57 | export const selectMenuCode = `import { SelectMenu, execute } from 'sunar';
58 |
59 | const selectMenu = new SelectMenu({ id: 'my-select-menu', type: 3 });
60 |
61 | execute(selectMenu, (interaction) => {
62 | interaction.reply({ content: 'Pong!' });
63 | });
64 |
65 | export { selectMenu };`;
66 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/hero.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | import { CopyInstall } from '@/components/copy-install';
4 | import { GridPattern } from '@/components/grid-pattern';
5 | import { Button } from '@/components/ui/button';
6 |
7 | import { cn } from '@/utils/cn';
8 |
9 | export function HomeHero() {
10 | return (
11 |
12 |
22 | span]:dark:from-muted-foreground [&>span]:dark:to-foreground [&>span]:dark:bg-gradient-to-t',
26 | '[&>span]:dark:to-40% [&>span]:dark:bg-clip-text [&>span]:dark:text-transparent',
27 | )}
28 | >
29 | Make Overpowered
30 | Discord Bots.
31 |
32 |
33 | Sunar emerges as a finely-tuned discord.js framework, meticulously
34 | engineered to prioritize{' '}
35 |
36 | ease of use and efficiency
37 |
38 | .
39 |
40 |
41 |
42 | Read the docs
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/highlights.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FolderTreeIcon,
3 | GlobeIcon,
4 | type LucideIcon,
5 | MousePointerClickIcon,
6 | RabbitIcon,
7 | ScaleIcon,
8 | TypeIcon,
9 | } from 'lucide-react';
10 |
11 | import { Cross } from '@/icons/cross';
12 |
13 | export function HomeHighlights() {
14 | return (
15 |
16 |
17 |
18 | Offers a simple and readable API, making Discord bot development
19 | straightforward for all users.
20 |
21 |
22 |
23 |
24 |
25 | Provides robust type definitions, ensuring developers in JavaScript
26 | and TypeScript enjoy a seamless experience.
27 |
28 |
29 |
30 |
31 |
32 | Prioritizes efficiency and performance while delivering essential
33 | Discord bot functionalities.
34 |
35 |
36 |
37 |
38 |
39 | Supports essential features like slash commands, context menu commands,
40 | and more for dynamic bot interactions.
41 |
42 |
43 | Sunar is open-source, allowing community contributions and customization
44 | to meet diverse bot development needs.
45 |
46 |
47 | Encourages clean and organized project architecture for easier
48 | maintenance and scalability.
49 |
50 |
51 | );
52 | }
53 |
54 | interface HighlightPros extends React.PropsWithChildren {
55 | title: string;
56 | icon: LucideIcon;
57 | }
58 |
59 | export function Highlight({ icon: IconComp, title, children }: HighlightPros) {
60 | return (
61 |
62 |
63 |
64 |
65 | {title}
66 |
67 |
68 |
{children}
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/horizontal-card.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import type { PropsWithChildren } from 'react';
3 |
4 | import Meteors from '@/components/meteors';
5 | import { cn } from '@/utils/cn';
6 |
7 | interface HorizontalSectionProps extends PropsWithChildren {
8 | title: string;
9 | description: React.ReactNode;
10 | docsLink: string;
11 | otherLink?: { name: string; link: string; external?: boolean };
12 | withMeteors?: boolean;
13 | titleClass?: string;
14 | }
15 |
16 | export function HorizontalSection({
17 | title,
18 | description,
19 | docsLink,
20 | otherLink,
21 | withMeteors,
22 | titleClass,
23 | children,
24 | }: HorizontalSectionProps) {
25 | return (
26 |
32 | {withMeteors && }
33 |
34 |
40 | {title}
41 |
42 |
43 | {description}
44 |
45 |
46 |
53 | Read more
54 |
55 | {otherLink && (
56 |
62 | {otherLink.name}
63 |
64 | )}
65 |
66 |
67 | {children}
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 | import { HomeLayout as Layout } from 'fumadocs-ui/home-layout';
3 |
4 | import { baseOptions } from '@/app/layout.config';
5 |
6 | export default function HomeLayout({
7 | children,
8 | }: {
9 | children: ReactNode;
10 | }): React.ReactElement {
11 | return (
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | function Footer(): React.ReactElement {
22 | return (
23 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | import { HomeHero } from '@/app/(home)/hero';
4 | import { HomeBuilders } from '@/app/(home)/builders';
5 |
6 | import { HomeHighlights } from './highlights';
7 | import { Separator } from './separator';
8 |
9 | import { CopyInstall } from '@/components/copy-install';
10 | import { Button } from '@/components/ui/button';
11 |
12 | export default function HomePage() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Build your bot at
20 |
21 | the speed of thought.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Start creating your bot now.
34 |
35 |
36 |
37 | Read the docs
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/docs/app/(home)/separator.tsx:
--------------------------------------------------------------------------------
1 | export function Separator() {
2 | return
;
3 | }
4 |
5 | export function SectionTitle({ content }: { content: React.ReactNode }) {
6 | return (
7 |
8 |
9 | {content}
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/apps/docs/app/api/og/Geist-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunarjs/sunar/293cf286523fec49f009f5585cef425cf3e31a2d/apps/docs/app/api/og/Geist-Bold.woff
--------------------------------------------------------------------------------
/apps/docs/app/api/search/route.ts:
--------------------------------------------------------------------------------
1 | import { source } from '@/app/source';
2 | import { createSearchAPI } from 'fumadocs-core/search/server';
3 |
4 | export const { GET } = createSearchAPI('advanced', {
5 | indexes: source.getPages().map((page) => ({
6 | title: page.data.title,
7 | structuredData: page.data.structuredData,
8 | id: page.url,
9 | url: page.url,
10 | })),
11 | });
12 |
--------------------------------------------------------------------------------
/apps/docs/app/docs/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from 'next/navigation';
2 | import {
3 | DocsBody,
4 | DocsCategory,
5 | DocsDescription,
6 | DocsPage,
7 | DocsTitle,
8 | } from 'fumadocs-ui/page';
9 |
10 | import { GitHubIcon } from '@/icons';
11 | import { source } from '@/app/source';
12 | import { createMetadata } from '@/utils/metadata';
13 | import { mdxComponents } from '@/utils/mdx-components';
14 |
15 | interface Params {
16 | slug: string[];
17 | }
18 |
19 | export const dynamicParams = false;
20 |
21 | export default async function Page({ params }: { params: Params }) {
22 | const page = source.getPage(params.slug);
23 |
24 | if (page == null) notFound();
25 |
26 | const path = `apps/docs/content/docs/${page.file.path}`;
27 |
28 | const footer = (
29 |
35 |
36 | Edit on Github ↗
37 |
38 | );
39 |
40 | return (
41 |
51 | {page.data.title}
52 | {page.data.description}
53 |
54 |
55 | {page.data.index ? (
56 |
57 | ) : null}
58 |
59 |
60 | );
61 | }
62 |
63 | export function generateStaticParams(): Params[] {
64 | return source.generateParams();
65 | }
66 |
67 | export async function generateMetadata({ params }: { params: Params }) {
68 | const page = source.getPage(params.slug);
69 |
70 | if (page == null) notFound();
71 |
72 | const description =
73 | page.data.description ??
74 | 'The library for building overpowered discord bots with discord.js.';
75 |
76 | const imageParams = new URLSearchParams();
77 | imageParams.set('title', page.data.title);
78 | imageParams.set('description', description);
79 |
80 | const image = {
81 | alt: 'Banner',
82 | url: `/api/og?${imageParams.toString()}`,
83 | width: 1200,
84 | height: 630,
85 | };
86 |
87 | return createMetadata({
88 | title: page.data.title,
89 | description,
90 | openGraph: {
91 | url: `/docs/${page.slugs.join('/')}`,
92 | images: image,
93 | },
94 | twitter: {
95 | images: image,
96 | },
97 | });
98 | }
99 |
--------------------------------------------------------------------------------
/apps/docs/app/docs/layout.tsx:
--------------------------------------------------------------------------------
1 | import { DocsLayout } from 'fumadocs-ui/layout';
2 | import type { ReactNode } from 'react';
3 |
4 | import { source } from '@/app/source';
5 | import { baseOptions } from '@/app/layout.config';
6 |
7 | export default function Layout({ children }: { children: ReactNode }) {
8 | return (
9 |
14 | {children}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/apps/docs/app/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --primary: 10 85% 59%;
7 | }
8 |
--------------------------------------------------------------------------------
/apps/docs/app/layout.config.tsx:
--------------------------------------------------------------------------------
1 | import type { B as BaseLayoutProps } from 'fumadocs-ui/dist/layout.shared-DEQFTB9M';
2 |
3 | import { Logo } from '@/components/logo';
4 | import { SunarIcon } from '@/components/icon';
5 |
6 | export const baseOptions: BaseLayoutProps = {
7 | githubUrl: 'https://github.com/sunarjs/sunar',
8 | nav: {
9 | title: ,
10 | transparentMode: 'top',
11 | },
12 | links: [
13 | {
14 | text: 'Documentation',
15 | icon: ,
16 | url: '/docs',
17 | active: 'nested-url',
18 | },
19 | ],
20 | };
21 |
--------------------------------------------------------------------------------
/apps/docs/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '@/app/global.css';
2 |
3 | import type { ReactNode } from 'react';
4 | import { GeistSans } from 'geist/font/sans';
5 | import { GeistMono } from 'geist/font/mono';
6 | import { RootProvider } from 'fumadocs-ui/provider';
7 | import { Analytics } from '@vercel/analytics/react';
8 |
9 | import { cn } from '@/utils/cn';
10 | import { baseUrl, createMetadata } from '@/utils/metadata';
11 |
12 | export const metadata = createMetadata({
13 | title: {
14 | template: '%s | Sunar',
15 | default: 'Sunar',
16 | },
17 | description: 'The discord.js framework for building discord bots.',
18 | metadataBase: baseUrl,
19 | });
20 |
21 | export default function Layout({ children }: { children: ReactNode }) {
22 | return (
23 |
28 |
29 | {children}
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/docs/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import type { MetadataRoute } from 'next';
2 | import { baseUrl } from '@/utils/metadata';
3 | import { source } from '@/app/source';
4 |
5 | export default function sitemap(): MetadataRoute.Sitemap {
6 | const url = (path: string): string => new URL(path, baseUrl).toString();
7 |
8 | return [
9 | {
10 | url: url('/'),
11 | changeFrequency: 'monthly',
12 | priority: 1,
13 | },
14 | {
15 | url: url('/docs'),
16 | changeFrequency: 'monthly',
17 | priority: 0.8,
18 | },
19 | ...source.getPages().map((page) => ({
20 | url: url(page.url),
21 | lastModified: page.data.lastModified
22 | ? new Date(page.data.lastModified)
23 | : undefined,
24 | changeFrequency: 'weekly',
25 | priority: 0.5,
26 | })),
27 | ];
28 | }
29 |
--------------------------------------------------------------------------------
/apps/docs/app/source.ts:
--------------------------------------------------------------------------------
1 | import { icons } from 'lucide-react';
2 | import { createElement } from 'react';
3 | import {
4 | type InferMetaType,
5 | type InferPageType,
6 | loader,
7 | } from 'fumadocs-core/source';
8 | import { createMDXSource } from 'fumadocs-mdx';
9 |
10 | import { meta, docs } from '@/.source';
11 | import { IconContainer } from '@/components/ui/icon';
12 |
13 | export const source = loader({
14 | baseUrl: '/docs',
15 | icon(icon) {
16 | if (icon && icon in icons)
17 | return createElement(IconContainer, {
18 | icon: icons[icon as keyof typeof icons],
19 | });
20 | },
21 | source: createMDXSource(docs, meta),
22 | });
23 |
24 | export type Page = InferPageType;
25 | export type Meta = InferMetaType;
26 |
--------------------------------------------------------------------------------
/apps/docs/components/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn';
2 | import * as Base from 'fumadocs-ui/components/codeblock';
3 | import { Jsx, toJsxRuntime } from 'hast-util-to-jsx-runtime';
4 | import { Fragment, type HTMLAttributes } from 'react';
5 | import {
6 | BundledLanguage,
7 | LanguageInput,
8 | SpecialLanguage,
9 | StringLiteralUnion,
10 | codeToHast,
11 | } from 'shiki';
12 | import { jsx, jsxs } from 'react/jsx-runtime';
13 |
14 | const langs = ['js'] satisfies (
15 | | LanguageInput
16 | | SpecialLanguage
17 | | StringLiteralUnion
18 | )[];
19 |
20 | export type CodeBlockProps = HTMLAttributes & {
21 | code: string;
22 | wrapper?: Base.CodeBlockProps;
23 | lang: (typeof langs)[number];
24 | };
25 |
26 | export async function CodeBlock({
27 | code,
28 | lang,
29 | wrapper,
30 | ...props
31 | }: CodeBlockProps) {
32 | const hast = await codeToHast(code, {
33 | lang,
34 | defaultColor: false,
35 | themes: {
36 | light: 'min-light',
37 | dark: 'github-dark-default',
38 | },
39 | });
40 |
41 | const rendered = toJsxRuntime(hast, {
42 | jsx: jsx as Jsx,
43 | jsxs: jsxs as Jsx,
44 | Fragment,
45 | development: false,
46 | components: {
47 | // @ts-expect-error -- JSX component
48 | pre: Base.Pre,
49 | },
50 | });
51 |
52 | return (
53 |
58 | {rendered}
59 |
60 | );
61 | }
62 |
63 | interface PrettyCodeBlockOptions extends CodeBlockProps {
64 | background: React.ReactNode;
65 | container?: React.DetailedHTMLProps<
66 | React.HTMLAttributes,
67 | HTMLDivElement
68 | >;
69 | }
70 |
71 | export function PrettyCodeBlock({
72 | background,
73 | container,
74 | ...props
75 | }: PrettyCodeBlockOptions) {
76 | return (
77 | div>canvas]:hidden [&>div>canvas]:sm:block',
81 | container?.className,
82 | )}
83 | >
84 | {background}
85 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/apps/docs/components/copy-install.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { CheckIcon, CopyIcon, TerminalIcon } from 'lucide-react';
4 | import { useState } from 'react';
5 |
6 | import { cn } from '@/utils/cn';
7 |
8 | const content = 'npm create sunar';
9 |
10 | export function CopyInstall(
11 | props: React.DetailedHTMLProps<
12 | React.HTMLAttributes,
13 | HTMLButtonElement
14 | >,
15 | ) {
16 | const [copied, setCopied] = useState(false);
17 |
18 | const handleClick = async () => {
19 | try {
20 | await navigator.clipboard.writeText(content);
21 |
22 | setCopied(true);
23 |
24 | setTimeout(() => {
25 | setCopied(false);
26 | }, 3000);
27 | } catch (error) {
28 | setCopied(false);
29 | console.error(error);
30 | }
31 | };
32 |
33 | return (
34 |
42 |
43 |
44 | {content}
45 |
46 |
47 |
53 |
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/apps/docs/components/grid-pattern.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn';
2 | import { useId } from 'react';
3 |
4 | interface GridPatternProps {
5 | width?: any;
6 | height?: any;
7 | x?: any;
8 | y?: any;
9 | squares?: Array<[x: number, y: number]>;
10 | strokeDasharray?: any;
11 | className?: string;
12 | [key: string]: any;
13 | }
14 |
15 | export function GridPattern({
16 | width = 40,
17 | height = 40,
18 | x = -1,
19 | y = -1,
20 | strokeDasharray = 0,
21 | squares,
22 | className,
23 | ...props
24 | }: GridPatternProps) {
25 | const id = useId();
26 |
27 | return (
28 |
36 |
37 |
45 |
50 |
51 |
52 |
53 | {squares && (
54 |
55 | {squares.map(([x, y]) => (
56 |
64 | ))}
65 |
66 | )}
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/apps/docs/components/icon.tsx:
--------------------------------------------------------------------------------
1 | export function SunarIcon(props: React.SVGProps) {
2 | return (
3 |
11 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/docs/components/logo.tsx:
--------------------------------------------------------------------------------
1 | import { SunarIcon } from './icon';
2 |
3 | export function Logo(props: React.SVGProps) {
4 | return (
5 |
9 |
10 | Sunar
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/apps/docs/components/meteors.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/utils/cn';
4 | import { useEffect, useState } from 'react';
5 |
6 | interface MeteorsProps {
7 | number?: number;
8 | }
9 | export const Meteors = ({ number = 20 }: MeteorsProps) => {
10 | const [meteorStyles, setMeteorStyles] = useState>(
11 | [],
12 | );
13 |
14 | useEffect(() => {
15 | const styles = [...new Array(number)].map(() => ({
16 | top: -5,
17 | left: Math.floor(Math.random() * window.innerWidth) + 'px',
18 | animationDelay: Math.random() * 1 + 0.2 + 's',
19 | animationDuration: Math.floor(Math.random() * 8 + 2) + 's',
20 | }));
21 | setMeteorStyles(styles);
22 | }, [number]);
23 |
24 | return (
25 | <>
26 | {[...meteorStyles].map((style, idx) => (
27 | // Meteor Head
28 |
35 | {/* Meteor Tail */}
36 |
37 |
38 | ))}
39 | >
40 | );
41 | };
42 |
43 | export default Meteors;
44 |
--------------------------------------------------------------------------------
/apps/docs/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn';
2 |
3 | export interface ButtonProps
4 | extends React.DetailedHTMLProps<
5 | React.HTMLAttributes,
6 | HTMLButtonElement
7 | > {}
8 |
9 | export function Button(props?: ButtonProps) {
10 | return (
11 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/docs/components/ui/icon.tsx:
--------------------------------------------------------------------------------
1 | import type { LucideIcon } from 'lucide-react';
2 | import { TerminalIcon } from 'lucide-react';
3 | import { type HTMLAttributes } from 'react';
4 |
5 | import { cn } from '@/utils/cn';
6 |
7 | export function IconContainer({
8 | icon: Icon,
9 | ...props
10 | }: HTMLAttributes & {
11 | icon?: LucideIcon;
12 | }): React.ReactElement {
13 | return (
14 |
21 | {Icon ? : }
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/docs/components/ui/links.tsx:
--------------------------------------------------------------------------------
1 | import { DiscordIcon, GitHubIcon, DiscordJSIcon } from '@/icons';
2 | import { PropsWithChildren } from 'react';
3 |
4 | type LinkIcon = 'github' | 'discordjs' | 'discord';
5 |
6 | interface LinkItem {
7 | icon: LinkIcon;
8 | link: string;
9 | label: string;
10 | }
11 |
12 | const Icons: Record = {
13 | discord: ,
14 | discordjs: ,
15 | github: ,
16 | };
17 |
18 | export function Links({ children }: PropsWithChildren) {
19 | return {children}
;
20 | }
21 |
22 | export function Link({ label, link, icon }: LinkItem) {
23 | return (
24 |
30 | {Icons[icon]}
31 | {label}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/builders/button.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Button
3 | description: Buttons are interactive elements users can click to trigger specific actions. They are ideal for creating interactive messages, such as confirmation prompts or menu navigation.
4 | ---
5 |
6 |
7 |
12 |
17 |
22 |
23 |
24 | ## Usage
25 |
26 | ```js
27 | import { Button, execute } from 'sunar';
28 |
29 | const button = new Button({ id: 'example' });
30 |
31 | execute(button, (interaction) => {
32 | // handle execution
33 | });
34 |
35 | export { button };
36 | ```
37 |
38 | ## Implementation
39 |
40 | The following example demonstrates how to implement a Button using Sunar:
41 |
42 | ```js
43 | import { Button, Slash, execute } from 'sunar';
44 | import {
45 | ActionRowBuilder,
46 | ButtonBuilder,
47 | ButtonStyle,
48 | PermissionFlagsBits,
49 | } from 'discord.js';
50 |
51 | const slash = new Slash({
52 | name: 'leave',
53 | description: 'Make the bot leave the server',
54 | dmPermission: false,
55 | defaultMemberPermissions: [PermissionFlagsBits.Administrator],
56 | });
57 |
58 | execute(slash, (interaction) => {
59 | const button = new ButtonBuilder()
60 | .setCustomId('confirmLeave')
61 | .setLabel('Leave')
62 | .setStyle(ButtonStyle.Danger);
63 |
64 | const row = new ActionRowBuilder().setComponents(button);
65 |
66 | interaction.reply({
67 | content: 'Are you certain about my leaving the server?',
68 | components: [row],
69 | });
70 | });
71 |
72 | const button = new Button({ id: 'confirmLeave' });
73 |
74 | execute(button, async (interaction) => {
75 | await interaction.reply({
76 | content: 'Leaving...',
77 | ephemeral: true,
78 | });
79 |
80 | interaction.guild.leave();
81 | });
82 |
83 | export { slash, button };
84 | ```
85 |
86 | ## Reference
87 |
88 | ### ButtonOptions [#button-options]
89 |
90 | ```json doc-gen:typescript
91 | {
92 | "file": "./content/docs/props.ts",
93 | "name": "ButtonOptions"
94 | }
95 | ```
96 |
97 | ### ButtonConfig [#button-config]
98 |
99 | ```json doc-gen:typescript
100 | {
101 | "file": "./content/docs/props.ts",
102 | "name": "ButtonConfig"
103 | }
104 | ```
--------------------------------------------------------------------------------
/apps/docs/content/docs/builders/context-menu.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: ContextMenu
3 | description: Context menu commands are available directly in the right-click context menu for users or messages. These commands are convenient for quick actions without needing to type a command.
4 | ---
5 |
6 |
7 |
12 |
17 |
22 |
27 |
32 |
33 |
34 | ## Usage
35 |
36 | ```js
37 | import { ContextMenu, execute } from 'sunar';
38 | import { ApplicationCommandType } from 'discord.js';
39 |
40 | const contextMenu = new ContextMenu({
41 | name: 'example',
42 | type: ApplicationCommandType.User,
43 | });
44 |
45 | execute(contextMenu, (interaction) => {
46 | // handle execution
47 | });
48 |
49 | export { contextMenu };
50 | ```
51 |
52 | ## Implementation
53 |
54 | The following example demonstrates how to implement a Context Menu command using Sunar:
55 |
56 | ```js copy
57 | import { ContextMenu, execute } from 'sunar';
58 | import { ApplicationCommandType } from 'discord.js';
59 |
60 | const contextMenu = new ContextMenu({
61 | name: 'Show avatar',
62 | type: ApplicationCommandType.User,
63 | });
64 |
65 | execute(contextMenu, (interaction) => {
66 | const avatarURL = interaction.targetUser.displayAvatarURL({
67 | size: 1024,
68 | forceStatic: false,
69 | });
70 |
71 | interaction.reply({
72 | content: `Avatar of user **${interaction.user.username}**`,
73 | files: [avatarURL],
74 | });
75 | });
76 |
77 | export { contextMenu };
78 | ```
79 |
80 | ## Reference
81 |
82 | ### ContextMenuConfig [#context-menu-config]
83 |
84 | ```json doc-gen:typescript
85 | {
86 | "file": "./content/docs/props.ts",
87 | "name": "ContextMenuConfig"
88 | }
89 | ```
--------------------------------------------------------------------------------
/apps/docs/content/docs/builders/group.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Group
3 | description: The Group class handles slash commands with subcommands, allowing for structured and efficient management of hierarchical commands under a single root.
4 | ---
5 |
6 |
7 |
12 |
17 |
22 |
23 |
24 | ## Usage
25 |
26 | ```js
27 | import { Group, execute } from 'sunar';
28 |
29 | const group = new Group('root', 'parent', 'sub');
30 |
31 | execute(group, (interaction) => {
32 | // handle execution
33 | });
34 |
35 | export { group };
36 | ```
37 |
38 | ## Implementation
39 |
40 | The following example demonstrates how to implement a Group using Sunar:
41 |
42 |
43 |
44 |
45 | ## Create the root command
46 |
47 | ```js title="src/commands/example/root.js"
48 | import { Slash } from 'sunar';
49 | import { ApplicationCommandOptionType } from 'discord.js';
50 |
51 | const slash = new Slash({
52 | name: 'example',
53 | description: 'this is a example',
54 | options: [
55 | {
56 | name: 'parent',
57 | description: 'this is the parent',
58 | type: ApplicationCommandOptionType.SubcommandGroup,
59 | options: [
60 | {
61 | name: 'sub',
62 | description: 'this is the sub',
63 | type: ApplicationCommandOptionType.Subcommand
64 | },
65 | ],
66 | },
67 | ],
68 | });
69 |
70 | export { slash };
71 | ```
72 |
73 |
74 |
75 | ## Create the sub command
76 |
77 | ```js title="src/commands/example/parent/sub.js"
78 | import { Group, execute } from 'sunar';
79 |
80 | const group = new Group('example', 'parent', 'sub');
81 |
82 | // protect(group, [...])
83 | // config(group, {...})
84 |
85 | execute(group, (interaction) => {
86 | // handle execution
87 | });
88 |
89 | export { group };
90 | ```
91 |
92 |
93 |
94 | The name and paths of the files do not affect how it works!
95 |
96 |
97 |
98 |
99 | ## Reference
100 |
101 | ### GroupConfig [#group-config]
102 |
103 | ```json doc-gen:typescript
104 | {
105 | "file": "./content/docs/props.ts",
106 | "name": "GroupConfig"
107 | }
108 | ```
--------------------------------------------------------------------------------
/apps/docs/content/docs/builders/modal.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Modal
3 | description: Modals are popup forms that can collect detailed user input. They are particularly useful for complex interactions that require multiple fields or steps.
4 | ---
5 |
6 |
7 |
12 |
17 |
22 |
23 |
24 | ## Usage
25 |
26 | ```js
27 | import { Modal, execute } from 'sunar';
28 |
29 | const modal = new Modal({ id: 'example' });
30 |
31 | execute(modal, (interaction) => {
32 | // handle execution
33 | });
34 |
35 | export { modal };
36 | ```
37 |
38 | ## Implementation
39 |
40 | The following example demonstrates how to implement a Modal using Sunar:
41 |
42 | ```js
43 | import { Modal, Slash, execute } from 'sunar';
44 | import {
45 | ActionRowBuilder,
46 | ModalBuilder,
47 | TextInputBuilder,
48 | TextInputStyle,
49 | } from 'discord.js';
50 |
51 | const slash = new Slash({
52 | name: 'feedback',
53 | description: 'Send us feedback',
54 | });
55 |
56 | execute(slash, (interaction) => {
57 | const contentInput = new TextInputBuilder()
58 | .setCustomId('content')
59 | .setLabel('Content')
60 | .setStyle(TextInputStyle.Paragraph)
61 | .setPlaceholder('Your feedback content...')
62 | .setRequired(true);
63 |
64 | const row = new ActionRowBuilder().setComponents(contentInput);
65 |
66 | const modal = new ModalBuilder()
67 | .setCustomId('feedback')
68 | .setTitle('Submit your feedback')
69 | .setComponents(row);
70 |
71 | interaction.showModal(modal);
72 | });
73 |
74 | const modal = new Modal({ id: 'feedback' });
75 |
76 | execute(modal, (interaction) => {
77 | const feedback = interaction.fields.getTextInputValue('content');
78 |
79 | // Send feedback somewhere...
80 |
81 | interaction.reply({
82 | content: 'Thanks for the feedback!',
83 | ephemeral: true,
84 | });
85 | });
86 |
87 | export { slash, modal };
88 | ```
89 |
90 | ## Reference
91 |
92 | ### ModalOptions [#modal-options]
93 |
94 | ```json doc-gen:typescript
95 | {
96 | "file": "./content/docs/props.ts",
97 | "name": "ModalOptions"
98 | }
99 | ```
100 |
101 | ### ModalConfig [#modal-config]
102 |
103 | ```json doc-gen:typescript
104 | {
105 | "file": "./content/docs/props.ts",
106 | "name": "ModalConfig"
107 | }
108 | ```
109 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/builders/protector.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Protector
3 | description: Protectors in Sunar act as middleware, allowing you to intercept and control the flow of commands and interactions within your Discord bot. They provide a flexible way to enforce permissions, validate inputs, or perform pre-processing before executing commands.
4 | ---
5 |
6 |
7 |
12 |
17 |
18 |
19 | ## Usage
20 |
21 | ```js
22 | import { Protector, execute } from 'sunar';
23 |
24 | const protector = new Protector({
25 | commands: ['slash'],
26 | });
27 |
28 | execute(protector, (arg) => {
29 | // handle execution
30 | });
31 |
32 | export { protector };
33 | ```
34 |
35 | ## Implementation
36 |
37 | In the [middlewares guide](/docs/guides/middlewares#protectors) you can find a detailed example of the implementation of protectors.
--------------------------------------------------------------------------------
/apps/docs/content/docs/builders/signal.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Signal
3 | description: Signals in Sunar correspond to events in discord.js. They allow you to handle various actions and responses that occur within your Discord bot, such as messages being sent, users joining or leaving, and more.
4 | ---
5 |
6 |
7 |
12 |
17 |
22 |
23 |
24 | ## Usage
25 |
26 | ```js
27 | import { Signal, execute } from 'sunar';
28 |
29 | const signal = new Signal('messageCreate');
30 |
31 | execute(signal, (message) => {
32 | // handle execution
33 | });
34 |
35 | export { signal };
36 | ```
37 |
38 | ## Implementation
39 |
40 | The following example demonstrates how to implement a Signal using Sunar:
41 |
42 | ```js
43 | import { Signal, execute } from 'sunar';
44 | import { TextChannel } from 'discord.js';
45 |
46 | const signal = new Signal('guildMemberAdd');
47 |
48 | execute(signal, (member) => {
49 | const channel = member.guild.channels.cache.find(
50 | (c) => c.name === 'welcomes',
51 | );
52 |
53 | if (!(channel instanceof TextChannel)) return;
54 |
55 | channel.send({ content: `${member} just joined!` });
56 | });
57 |
58 | export { signal };
59 | ```
60 |
61 | ## Reference
62 |
63 | ### SignalOptions [#signal-options]
64 |
65 | ```json doc-gen:typescript
66 | {
67 | "file": "./content/docs/props.ts",
68 | "name": "SignalOptions"
69 | }
70 | ```
71 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/builders/slash.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Slash
3 | description: Slash commands are one of the primary ways users interact with bots. They provide a structured way for users to issue commands directly within the chat interface.
4 | ---
5 |
6 |
7 |
12 |
17 |
22 |
27 |
28 |
29 | ## Usage
30 |
31 | ```js
32 | import { Slash, execute } from 'sunar';
33 |
34 | const slash = new Slash({
35 | name: 'example',
36 | description: 'example description',
37 | });
38 |
39 | execute(slash, (interaction) => {
40 | // handle execution
41 | });
42 |
43 | export { slash };
44 | ```
45 |
46 | ## Implementation
47 |
48 | The following example demonstrates how to implement a Slash command using Sunar:
49 |
50 | ```js
51 | import { Slash, execute } from 'sunar';
52 | import { ApplicationCommandOptionType } from 'discord.js';
53 |
54 | const slash = new Slash({
55 | name: 'avatar',
56 | description: 'Show user avatar',
57 | options: [
58 | {
59 | name: 'target',
60 | description: 'Target user',
61 | type: ApplicationCommandOptionType.User,
62 | },
63 | ],
64 | });
65 |
66 | execute(slash, (interaction) => {
67 | const user = interaction.options.getUser('target') ?? interaction.user;
68 |
69 | const avatarURL = user.displayAvatarURL({
70 | size: 1024,
71 | forceStatic: false,
72 | });
73 |
74 | interaction.reply({
75 | content: `Avatar of user **${interaction.user.username}**`,
76 | files: [avatarURL],
77 | });
78 | });
79 |
80 | export { slash };
81 | ```
82 |
83 | ## Reference
84 |
85 | ### SlashConfig [#slash-config]
86 |
87 | ```json doc-gen:typescript
88 | {
89 | "file": "./content/docs/props.ts",
90 | "name": "SlashConfig"
91 | }
92 | ```
--------------------------------------------------------------------------------
/apps/docs/content/docs/guides/interactions-handling.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Interactions handling
3 | description: Learn how to effectively manage interactions between your bot and users using Sunar. From responding to slash commands, context menu clicks, button presses, to select menu choices, Sunar offers robust mechanisms to handle diverse interactions within Discord.
4 | ---
5 |
6 | ## Handle all supported interactions
7 |
8 | This setup configures Sunar to efficiently manage various Discord interactions using a `Signal` named `interactionCreate`. It prepares the framework to handle incoming events such as slash commands, buttons, and other interactions initiated by users in Discord.
9 |
10 |
11 | ```js title="src/signals/interaction-create.js"
12 | import { Signal, execute } from 'sunar';
13 | import { handleInteraction } from 'sunar/handlers';
14 |
15 | const signal = new Signal('interactionCreate');
16 |
17 | execute(signal, async (interaction) => {
18 | await handleInteraction(interaction);
19 | });
20 |
21 | export { signal };
22 | ```
23 |
24 | ## Handle only specific interactions
25 |
26 | This configuration sets up Sunar to selectively manage and respond to specific types of Discord interactions based on their nature, such as slash commands, context menu commands, buttons, modals, select menus, and autocomplete commands.
27 |
28 | ```js title="src/signals/interaction-create.js"
29 | import { Signal, execute } from 'sunar';
30 |
31 | import {
32 | handleAutocomplete,
33 | handleModal,
34 | handleSelectMenu,
35 | handleSlash,
36 | handleButton,
37 | handleContextMenu,
38 | } from 'sunar/handlers';
39 |
40 | const signal = new Signal('interactionCreate');
41 |
42 | execute(signal, async (interaction) => {
43 | if (interaction.isChatInputCommand()) await handleSlash(interaction); // [!code highlight]
44 | if (interaction.isContextMenuCommand()) await handleContextMenu(interaction); // [!code highlight]
45 | if (interaction.isButton()) await handleButton(interaction); // [!code highlight]
46 | if (interaction.isModalSubmit()) await handleModal(interaction); // [!code highlight]
47 | if (interaction.isAnySelectMenu()) await handleSelectMenu(interaction); // [!code highlight]
48 | if (interaction.isAutocomplete()) await handleAutocomplete(interaction); // [!code highlight]
49 | });
50 |
51 | export { signal };
52 | ```
53 |
54 | This setup allows your bot to efficiently manage and respond to specific interaction types on Discord, ensuring accurate and timely responses tailored to the user's actions. Customize the handling logic within each handler function (`handleSlash`, `handleContextMenu`, etc.) as needed to implement your bot's functionality and interaction flow according to Discord's API capabilities and user experience requirements.
--------------------------------------------------------------------------------
/apps/docs/content/docs/guides/load-modules.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Load modules
3 | description: Modules in Sunar encapsulate related commands, signals and components, enabling efficient organization and management of your bot's capabilities. Learn how to leverage Sunar's modular architecture to streamline development and scalability.
4 | ---
5 |
6 | ## Example
7 |
8 | All you have to do is use the `load` function and use a [glob](https://www.npmjs.com/package/glob) pattern:
9 |
10 | ```js title="src/index.js"
11 | import { Client, load } from 'sunar';
12 |
13 | const client = new Client(/* your options */);
14 |
15 | const start = async () => {
16 | // you can add more directories to load by
17 | // passing them with a comma after "signals"
18 | await load('src/{commands,signals}/**/*.{js,ts}'); // [!code highlight]
19 |
20 | return client.login('YOUR_DISCORD_BOT_TOKEN');
21 | };
22 |
23 | start();
24 | ```
25 |
26 |
27 | Replace `'YOUR_DISCORD_BOT_TOKEN'` with your actual bot token obtained from the [Discord Developer Portal](https://discord.com/developers/applications).
28 |
29 |
30 |
31 | If you are using ECMAScript modules, you can use the [top-level await](https://v8.dev/features/top-level-await) feature and avoid the `start` function.
32 |
33 |
34 | ## File structure
35 |
36 | Here's an overview of the project's file structure to help you visualize how files are organized within Sunar.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/guides/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "load-modules",
4 | "working-with-signals",
5 | "interactions-handling",
6 | "registering-commands",
7 | "middlewares",
8 | "implementing-cooldowns"
9 | ]
10 | }
--------------------------------------------------------------------------------
/apps/docs/content/docs/guides/middlewares/owner-only.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Owner only
3 | description: Learn how to restrict command execution to only the bot owner using middleware in Sunar. This guide provides examples to enforce owner-only access.
4 | ---
5 |
6 | ## Usage
7 |
8 | ```js
9 | protect(builder, [ownerOnly])
10 | ```
11 |
12 | ## Logic
13 |
14 | ```js
15 | import { Message } from 'discord.js';
16 | import { Protector, execute } from 'sunar';
17 |
18 | const OWNERS = ['123', '456', '789'];
19 |
20 | const ownerOnly = new Protector({
21 | commands: ['autocomplete', 'contextMenu', 'slash'],
22 | components: ['button', 'modal', 'selectMenu'],
23 | signals: ['interactionCreate', 'messageCreate'],
24 | });
25 |
26 | const content = 'Only the bot owners can use this.';
27 |
28 | execute(ownerOnly, (arg, next) => {
29 | const entry = Array.isArray(arg) ? arg[0] : arg;
30 |
31 | if (entry instanceof Message) {
32 | const isOwner = OWNERS.includes(entry.author.id);
33 | if (isOwner) return next();
34 | return entry.reply({ content });
35 | }
36 |
37 | const isOwner = OWNERS.includes(entry.user.id);
38 |
39 | if (entry.isAutocomplete() && !isOwner) return entry.respond([]);
40 | if (entry.isRepliable() && !isOwner) return entry.reply({ content, ephemeral: true });
41 |
42 | return isOwner && next();
43 | });
44 |
45 | export { ownerOnly };
46 | ```
--------------------------------------------------------------------------------
/apps/docs/content/docs/guides/registering-commands/dynamic.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dynamic Registration
3 | description: Dynamic registration allows you to programmatically register commands either globally or per server, providing flexibility in managing your bot's commands based on runtime conditions or configurations.
4 | ---
5 |
6 | ```js title="src/signals/ready.js"
7 | import { Signal, execute } from 'sunar';
8 | import { registerCommands } from 'sunar/registry';
9 |
10 | const signal = new Signal('ready', { once: true });
11 |
12 | execute(signal, async (client) => {
13 | await registerCommands(client.application);
14 |
15 | console.log(`${client.user.tag} logged!`);
16 | });
17 |
18 | export { signal };
19 | ```
20 |
21 | ## Specific Guilds IDs commands
22 |
23 | To specify the servers on which a command should be registered you must pass it to them using the [config](/docs/mutators/config) mutator.
24 |
25 | ```js title="src/commands/ping.js"
26 | import { Slash, execute, config } from 'sunar';
27 |
28 | const slash = new Slash({
29 | name: 'ping',
30 | description: "Ping the bot to check if it's online.",
31 | });
32 |
33 | config(slash, { // [!code highlight]
34 | guildsIds: ['YOUR_GUILD_ID'], // [!code highlight]
35 | }); // [!code highlight]
36 |
37 | execute(slash, (interaction) => {
38 | interaction.reply('Pong!');
39 | });
40 |
41 | export { slash };
42 | ```
43 |
44 | Replace `'YOUR_GUILD_ID'` with your actual guild IDs.
45 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/guides/registering-commands/global.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Global Registration
3 | description: Global registration allows commands to be registered universally across all servers where the bot is present, simplifying deployment and ensuring consistency in command availability across Discord communities.
4 | ---
5 |
6 | ```js title="src/signals/ready.js"
7 | import { Signal, execute } from 'sunar';
8 | import { registerGlobalCommands } from 'sunar/registry';
9 |
10 | const signal = new Signal('ready', { once: true });
11 |
12 | execute(signal, async (client) => {
13 | await registerGlobalCommands(client.application);
14 |
15 | console.log(`${client.user.tag} logged!`);
16 | });
17 |
18 | export { signal };
19 | ```
20 |
21 |
22 | Using this method is ideal for production environments as it ensures your commands are accessible across all servers your bot is in.
23 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/guides/registering-commands/guilds.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Guild Specific Registration
3 | description: Guilds registration involves registering all commands specifically for a list of servers passed directly through a function call, ensuring commands are tailored and accessible only to designated servers.
4 | ---
5 |
6 | ```js title="src/signals/ready.js"
7 | import { Signal, execute } from 'sunar';
8 | import { registerGuildCommands } from 'sunar/registry';
9 |
10 | const signal = new Signal('ready', { once: true });
11 |
12 | execute(signal, async (client) => {
13 | await registerGuildCommands(client.application, ['YOUR_GUILD_ID']);
14 |
15 | console.log(`${client.user.tag} logged!`);
16 | });
17 |
18 | export { signal };
19 | ```
20 |
21 | Replace `'YOUR_GUILD_ID'` with your actual guild IDs.
22 |
23 |
24 | It is recommended to use this method only in development environments as it allows you to test commands without affecting all servers your bot is in.
25 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "pages": [
4 | "index",
5 | "getting-started",
6 |
7 | "---Guides---",
8 | "...guides",
9 |
10 | "---Builders---",
11 | "...builders",
12 |
13 | "---Mutators---",
14 | "...mutators",
15 |
16 | "---Other---",
17 | "[discord.js](https://discord.js.org)",
18 | "[Ko-fi](https://ko-fi.com/tai03)",
19 | "[Buy Me a Coffee](https://buymeacoffee.com/tai03)"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/mutators/config.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: config
3 | description: Configure builders and fine-tune your discord bot's functionality with the flexible "config" mutator in Sunar. Customize settings, adjust parameters, and optimize your bot's behavior seamlessly.
4 | ---
5 |
6 |
7 |
8 |
9 |
10 | ## Usage
11 |
12 | ```js title="src/commands/ping.js"
13 | import { Slash, execute, config } from 'sunar';
14 |
15 | const slash = new Slash({
16 | name: 'ping',
17 | description: "Ping the bot to check if it's online.",
18 | });
19 |
20 | config(slash, { // [!code highlight]
21 | cooldown: 5000, // [!code highlight]
22 | guildsIds: [/* ... */], // [!code highlight]
23 | }); // [!code highlight]
24 |
25 | execute(slash, (interaction) => {
26 | interaction.reply('Pong!');
27 | });
28 |
29 | export { slash };
30 | ```
31 |
32 | import { Hourglass, Boxes } from 'lucide-react'
33 |
34 | ## Implementation
35 |
36 |
37 | }
39 | title="Implementing cooldowns"
40 | description="Learn how to effectively manage command usage by implementing cooldowns to prevent spam and control interaction frequency."
41 | href="/docs/guides/implementing-cooldowns"
42 | />
43 | }
45 | title="Dynamic registration"
46 | description="Explore dynamic command registration methods tailored for specific guilds or IDs, enabling flexible bot customization and interaction management."
47 | href="/docs/guides/registering-commands/dynamic#specific-guilds-ids-commands"
48 | />
49 |
50 |
51 | ## Reference
52 |
53 | ### config [#mutator]
54 |
55 | ```json doc-gen:typescript
56 | {
57 | "file": "./content/docs/props.ts",
58 | "name": "IConfigMut"
59 | }
60 | ```
61 |
62 | ### CooldownConfig [#cooldown-config]
63 |
64 | ```json doc-gen:typescript
65 | {
66 | "file": "./content/docs/props.ts",
67 | "name": "CooldownConfig"
68 | }
69 | ```
70 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/mutators/execute.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: execute
3 | description: Whether managing slash commands, context menus, or other interactive components, "execute" empowers you to define and execute custom logic effortlessly, enhancing user engagement and bot functionality.
4 | ---
5 |
6 |
7 |
12 |
13 |
14 | ## Usage
15 |
16 | ```js title="src/commands/ping.js"
17 | import { Slash, execute } from 'sunar';
18 |
19 | const slash = new Slash({
20 | name: 'ping',
21 | description: "Ping the bot to check if it's online.",
22 | });
23 |
24 | execute(slash, (interaction) => { // [!code highlight]
25 | interaction.reply('Pong!'); // [!code highlight]
26 | }); // [!code highlight]
27 |
28 | export { slash };
29 | ```
30 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/mutators/protect.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: protect
3 | description: Discover the "protect" mutator in Sunar, designed to enforce permissions and safeguard interactions within your Discord bot. By integrating "protect" into your commands and interactions, you can ensure that only authorized users or roles can access specific functionalities, enhancing security and control over bot operations.
4 | ---
5 |
6 |
7 |
8 |
9 |
10 | ## Usage
11 |
12 | ```js title="src/commands/ping.js"
13 | import { Slash, execute, protect } from 'sunar';
14 |
15 | const slash = new Slash({
16 | name: 'ping',
17 | description: "Ping the bot to check if it's online.",
18 | });
19 |
20 | protect(slash, [/* your protectors */]); // [!code highlight]
21 |
22 | execute(slash, (interaction) => {
23 | interaction.reply('Pong!');
24 | });
25 |
26 | export { slash };
27 | ```
28 |
29 | import { Ampersands, ShieldPlus } from 'lucide-react'
30 |
31 | ## Implementation
32 |
33 |
34 | }
36 | title="Create a protected command"
37 | description="Learn how to implement the 'protect' mutator with a detailed example, ensuring secure access control for commands."
38 | href="/docs/guides/middlewares#create-a-protected-command"
39 | />
40 | }
42 | title="Create the protector logic"
43 | description="Explore an example demonstrating the creation and integration of a protector logic to enforce specific conditions or permissions."
44 | href="/docs/guides/middlewares#create-the-protector-logic"
45 | />
46 |
47 |
--------------------------------------------------------------------------------
/apps/docs/content/docs/props.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | config,
3 | execute,
4 | protect,
5 | CooldownResolvable,
6 | } from 'sunar';
7 |
8 | export type {
9 | AutocompleteOptions,
10 | ContextMenuData,
11 | SignalOptions,
12 | ButtonOptions,
13 | SelectMenuOptions,
14 | ModalOptions,
15 | } from 'sunar';
16 |
17 | export type {
18 | ContextMenuConfig,
19 | ModalConfig,
20 | SlashConfig,
21 | ButtonConfig,
22 | SelectMenuConfig,
23 | CooldownConfig,
24 | GroupConfig,
25 | } from 'sunar';
26 |
27 | export interface ICooldownResolvable {
28 | CooldownResolvable: CooldownResolvable
29 | }
30 |
31 | export interface IExecuteMut {
32 | execute: typeof execute
33 | }
34 |
35 | export interface IProtectMut {
36 | protect: typeof protect
37 | }
38 |
39 | export interface IConfigMut {
40 | config: typeof config
41 | }
--------------------------------------------------------------------------------
/apps/docs/icons/buymeacoffee.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from 'react';
2 |
3 | export const BuyMeACoffeeIcon = (props: SVGProps) => (
4 |
5 |
9 |
13 |
14 |
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/apps/docs/icons/cross.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn';
2 |
3 | export function Cross({
4 | className,
5 | ...props
6 | }: React.DetailedHTMLProps<
7 | React.HTMLAttributes,
8 | HTMLDivElement
9 | >) {
10 | return (
11 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/apps/docs/icons/discord.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from 'react';
2 |
3 | export const DiscordIcon = (props: SVGProps) => (
4 |
12 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/apps/docs/icons/github.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from 'react';
2 |
3 | export function GitHubIcon(props: SVGProps) {
4 | return (
5 |
12 | GitHub
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/apps/docs/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from './buymeacoffee';
2 | export * from './cross';
3 | export * from './discord';
4 | export * from './discordjs';
5 | export * from './github';
6 | export * from './ko-fi';
7 | export * from './npm';
8 |
--------------------------------------------------------------------------------
/apps/docs/icons/ko-fi.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from 'react';
2 |
3 | export const KoFiIcon = (props: SVGProps) => (
4 |
10 |
17 |
18 |
22 |
23 |
31 |
32 | );
33 |
--------------------------------------------------------------------------------
/apps/docs/icons/npm.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { SVGProps } from 'react';
3 |
4 | export const NPMIcon = (props: SVGProps) => (
5 |
12 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/apps/docs/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse, type NextRequest } from 'next/server';
2 |
3 | export function middleware(request: NextRequest) {
4 | const url = request.nextUrl.clone();
5 |
6 | const isProduction = process.env.NODE_ENV === 'production';
7 | const requestedHost = request.headers.get('X-Forwarded-Host');
8 | const isLocalHost = requestedHost?.startsWith('localhost:');
9 |
10 | if (
11 | isProduction &&
12 | requestedHost &&
13 | !isLocalHost &&
14 | !requestedHost.match(/sunar.js.org/)
15 | ) {
16 | const host = 'sunar.js.org';
17 |
18 | const requestedPort = request.headers.get('X-Forwarded-Port');
19 | const requestedProto = request.headers.get('X-Forwarded-Proto');
20 |
21 | url.host = host;
22 | url.protocol = requestedProto || url.protocol;
23 | url.port = requestedPort || url.port;
24 |
25 | return NextResponse.redirect(url);
26 | }
27 |
28 | return NextResponse.next();
29 | }
30 |
31 | export const config = {
32 | matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
33 | };
34 |
--------------------------------------------------------------------------------
/apps/docs/next.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { createMDX } from 'fumadocs-mdx/next';
3 |
4 | /** @type {import('next').NextConfig} */
5 | const config = {
6 | reactStrictMode: true,
7 | };
8 |
9 | const withMDX = createMDX();
10 |
11 | export default withMDX(config);
12 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "pnpm build:docs && next build",
7 | "dev": "next dev",
8 | "start": "next start",
9 | "format": "prettier --write .",
10 | "build:docs": "tsx ./scripts/generate-docs.mts"
11 | },
12 | "dependencies": {
13 | "@vercel/analytics": "1.3.1",
14 | "fumadocs-core": "13.4.10",
15 | "fumadocs-docgen": "1.2.0",
16 | "fumadocs-mdx": "10.0.2",
17 | "fumadocs-typescript": "2.1.0",
18 | "fumadocs-ui": "13.4.10",
19 | "geist": "1.3.1",
20 | "hast-util-to-jsx-runtime": "^2.3.0",
21 | "lucide-react": "0.446.0",
22 | "next": "14.2.13",
23 | "react": "18.3.1",
24 | "react-dom": "18.3.1",
25 | "shiki": "1.20.0",
26 | "sunar": "workspace:*",
27 | "tailwind-merge": "2.5.2",
28 | "zod": "3.23.8"
29 | },
30 | "devDependencies": {
31 | "@types/mdx": "2.0.13",
32 | "@types/react": "18.3.10",
33 | "@types/react-dom": "18.3.0",
34 | "autoprefixer": "10.4.20",
35 | "postcss": "8.4.47",
36 | "prettier": "3.3.3",
37 | "prettier-plugin-tailwindcss": "0.6.8",
38 | "tailwindcss": "3.4.13",
39 | "tsx": "4.19.1",
40 | "typescript": "5.6.2"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/apps/docs/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/apps/docs/public/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunarjs/sunar/293cf286523fec49f009f5585cef425cf3e31a2d/apps/docs/public/banner.png
--------------------------------------------------------------------------------
/apps/docs/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/docs/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
4 | Host: https://sunar.vercel.app
5 |
6 | Sitemap: https://sunar.vercel.app/sitemap.xml
--------------------------------------------------------------------------------
/apps/docs/public/simple-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunarjs/sunar/293cf286523fec49f009f5585cef425cf3e31a2d/apps/docs/public/simple-banner.png
--------------------------------------------------------------------------------
/apps/docs/scripts/generate-docs.mts:
--------------------------------------------------------------------------------
1 | import * as Typescript from 'fumadocs-typescript';
2 | import * as path from 'node:path';
3 |
4 | const demoRegex = /^---type-table-demo---\r?\n(?.+)\r?\n---end---$/gm;
5 | void Typescript.generateFiles({
6 | input: ['./content/docs/**/*.model.mdx'],
7 | transformOutput(_, content) {
8 | return content.replace(demoRegex, '---type-table---\n$1\n---end---');
9 | },
10 | output: (file) =>
11 | path.resolve(
12 | path.dirname(file),
13 | `${path.basename(file).split('.')[0]}.mdx`,
14 | ),
15 | });
16 |
--------------------------------------------------------------------------------
/apps/docs/source.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineConfig,
3 | defineDocs,
4 | frontmatterSchema,
5 | metaSchema,
6 | } from 'fumadocs-mdx/config';
7 | import {
8 | fileGenerator,
9 | remarkDocGen,
10 | remarkInstall,
11 | typescriptGenerator,
12 | } from 'fumadocs-docgen';
13 | import { z } from 'zod';
14 |
15 | export const { docs, meta } = defineDocs({
16 | docs: {
17 | schema: frontmatterSchema.extend({
18 | index: z.boolean().default(false),
19 | }),
20 | },
21 | meta: {
22 | schema: metaSchema.extend({
23 | description: z.string().optional(),
24 | }),
25 | },
26 | });
27 |
28 | export default defineConfig({
29 | lastModifiedTime: 'git',
30 | mdxOptions: {
31 | rehypeCodeOptions: {
32 | themes: {
33 | light: 'min-light',
34 | dark: 'github-dark-dimmed',
35 | },
36 | },
37 | remarkPlugins: [
38 | [remarkInstall, { Tabs: 'InstallTabs' }],
39 | [remarkDocGen, { generators: [typescriptGenerator(), fileGenerator()] }],
40 | ],
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/apps/docs/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import { createPreset, presets } from 'fumadocs-ui/tailwind-plugin';
2 | import { fontFamily } from 'tailwindcss/defaultTheme';
3 |
4 | /** @type {import('tailwindcss').Config} */
5 | export default {
6 | content: [
7 | './components/**/*.{ts,tsx}',
8 | './icons/**/*.{ts,tsx}',
9 | './app/**/*.{ts,tsx}',
10 | './content/**/*.{md,mdx}',
11 | './mdx-components.{ts,tsx}',
12 | './node_modules/fumadocs-ui/dist/**/*.js',
13 | ],
14 | theme: {
15 | extend: {
16 | fontFamily: {
17 | sans: ['var(--font-geist-sans)', ...fontFamily.sans],
18 | mono: ['var(--font-geist-mono)', ...fontFamily.mono],
19 | },
20 | animation: {
21 | 'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear',
22 | slide: 'slide var(--speed) ease-in-out infinite alternate',
23 | meteor: 'meteor 5s linear infinite',
24 | },
25 | keyframes: {
26 | 'spin-around': {
27 | '0%': {
28 | transform: 'translateZ(0) rotate(0)',
29 | },
30 | '15%, 35%': {
31 | transform: 'translateZ(0) rotate(90deg)',
32 | },
33 | '65%, 85%': {
34 | transform: 'translateZ(0) rotate(270deg)',
35 | },
36 | '100%': {
37 | transform: 'translateZ(0) rotate(360deg)',
38 | },
39 | },
40 | slide: {
41 | to: {
42 | transform: 'translate(calc(100cqw - 100%), 0)',
43 | },
44 | },
45 | meteor: {
46 | '0%': { transform: 'rotate(215deg) translateX(0)', opacity: 1 },
47 | '70%': { opacity: 1 },
48 | '100%': {
49 | transform: 'rotate(215deg) translateX(-500px)',
50 | opacity: 0,
51 | },
52 | },
53 | },
54 | },
55 | },
56 | presets: [
57 | createPreset({
58 | addGlobalColors: true,
59 | preset: {
60 | ...presets.default,
61 | dark: {
62 | ...presets.default.dark,
63 | background: '0 0% 2%',
64 | foreground: '0 0% 98%',
65 | popover: '0 0% 4%',
66 | card: '0 0% 4%',
67 | muted: '0 0% 8%',
68 | border: '0 0% 14%',
69 | accent: '0 0% 15%',
70 | 'accent-foreground': '0 0% 100%',
71 | 'muted-foreground': '0 0% 60%',
72 | },
73 | },
74 | }),
75 | ],
76 | };
77 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "paths": {
19 | "@/*": ["./*"]
20 | },
21 | "plugins": [{ "name": "next" }]
22 | },
23 | "include": [
24 | "next-env.d.ts",
25 | "**/*.ts",
26 | "**/*.tsx",
27 | ".next/types/**/*.ts",
28 | "scripts/**/*.mts",
29 | "mdx/**/*.mjs"
30 | ],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------
/apps/docs/utils/cn.ts:
--------------------------------------------------------------------------------
1 | export { twMerge as cn } from 'tailwind-merge';
2 |
--------------------------------------------------------------------------------
/apps/docs/utils/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import type { MDXComponents } from 'mdx/types';
2 | import defaultComponents from 'fumadocs-ui/mdx';
3 |
4 | import { Callout } from 'fumadocs-ui/components/callout';
5 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
6 | import { TypeTable } from 'fumadocs-ui/components/type-table';
7 | import { Steps, Step } from 'fumadocs-ui/components/steps';
8 | import { File, Folder, Files } from 'fumadocs-ui/components/files';
9 |
10 | import { Links, Link } from '@/components/ui/links';
11 |
12 | export const mdxComponents: MDXComponents = {
13 | ...defaultComponents,
14 | Tabs,
15 | Tab,
16 | Callout,
17 | TypeTable,
18 | Step,
19 | Steps,
20 | Files,
21 | Folder,
22 | File,
23 | Links,
24 | Link,
25 | InstallTabs: ({
26 | items,
27 | children,
28 | }: {
29 | items: string[];
30 | children: React.ReactNode;
31 | }) => (
32 |
33 | {children}
34 |
35 | ),
36 | LanguageTabs: ({ children }: { children: React.ReactNode }) => (
37 |
38 | {children}
39 |
40 | ),
41 | };
42 |
--------------------------------------------------------------------------------
/apps/docs/utils/metadata.ts:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next/types';
2 |
3 | export function createMetadata(override: Metadata): Metadata {
4 | return {
5 | ...override,
6 | openGraph: {
7 | title: override.title ?? undefined,
8 | description: override.description ?? undefined,
9 | url: 'https://sunar.vercel.app',
10 | images: '/banner.png',
11 | siteName: 'Sunar',
12 | ...override.openGraph,
13 | },
14 | twitter: {
15 | card: 'summary_large_image',
16 | creator: '@imtai03',
17 | title: override.title ?? undefined,
18 | description: override.description ?? undefined,
19 | images: '/banner.png',
20 | ...override.twitter,
21 | },
22 | icons: {
23 | icon: '/icon.svg',
24 | },
25 | };
26 | }
27 |
28 | export const baseUrl =
29 | process.env.NODE_ENV === 'development'
30 | ? new URL('http://localhost:3000')
31 | : new URL(`https://${process.env.VERCEL_URL!}`);
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sunar/sunar",
3 | "description": "Discord.js lightweight framework.",
4 | "version": "0.0.0",
5 | "author": "tai (https://github.com/taii03)",
6 | "type": "module",
7 | "private": true,
8 | "scripts": {
9 | "build": "turbo run build --concurrency=4",
10 | "lint": "turbo run lint",
11 | "format": "turbo run format",
12 | "test": "turbo run test"
13 | },
14 | "dependencies": {
15 | "turbo": "^2.0.6"
16 | },
17 | "keywords": [
18 | "discord",
19 | "api",
20 | "bot",
21 | "client",
22 | "node",
23 | "discord.js",
24 | "framework",
25 | "handler",
26 | "typescript"
27 | ],
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/sunarjs/sunar.git"
31 | },
32 | "bugs": {
33 | "url": "https://github.com/sunarjs/sunar/issues"
34 | },
35 | "homepage": "https://github.com/sunarjs/sunar",
36 | "packageManager": "pnpm@9.5.0"
37 | }
38 |
--------------------------------------------------------------------------------
/packages/create-sunar/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .turbo
3 | dist
4 |
--------------------------------------------------------------------------------
/packages/create-sunar/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "biomejs.biome",
3 | "typescript.tsdk": "node_modules\\typescript\\lib",
4 | "cSpell.words": ["colorette", "outro", "typesafe"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/create-sunar/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sunar JavaScript Community
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/create-sunar/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
3 | "organizeImports": {
4 | "enabled": true
5 | },
6 | "linter": {
7 | "enabled": true,
8 | "rules": {
9 | "complexity": {
10 | "all": true
11 | },
12 | "correctness": {
13 | "recommended": true
14 | },
15 | "nursery": {
16 | "recommended": true
17 | },
18 | "performance": {
19 | "all": true,
20 | "noReExportAll": "off"
21 | },
22 | "security": {
23 | "all": true
24 | },
25 | "style": {
26 | "recommended": true
27 | },
28 | "suspicious": {
29 | "all": true,
30 | "noConsole": "off",
31 | "noConsoleLog": "off"
32 | }
33 | }
34 | },
35 | "formatter": {
36 | "enabled": true,
37 | "indentStyle": "tab",
38 | "indentWidth": 2,
39 | "lineWidth": 80
40 | },
41 | "javascript": {
42 | "formatter": {
43 | "enabled": true,
44 | "quoteStyle": "single",
45 | "indentWidth": 2,
46 | "lineWidth": 100,
47 | "semicolons": "always",
48 | "trailingCommas": "all"
49 | }
50 | },
51 | "files": {
52 | "include": ["**/*"],
53 | "ignore": ["node_modules", "dist"]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/create-sunar/configs/static-biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "organizeImports": {
3 | "enabled": true
4 | },
5 | "linter": {
6 | "enabled": true,
7 | "rules": {
8 | "recommended": true
9 | }
10 | },
11 | "formatter": {
12 | "enabled": true,
13 | "indentStyle": "tab",
14 | "indentWidth": 4,
15 | "lineWidth": 80
16 | },
17 | "javascript": {
18 | "formatter": {
19 | "enabled": true,
20 | "quoteStyle": "single",
21 | "indentWidth": 4,
22 | "lineWidth": 100,
23 | "semicolons": "always",
24 | "trailingCommas": "all"
25 | }
26 | },
27 | "files": {
28 | "include": ["**/*"],
29 | "ignore": ["node_modules", "dist"]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/create-sunar/configs/static-eslint-js.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { es2022: true },
4 | extends: ['eslint:recommended'],
5 | ignorePatterns: ['.eslintrc.cjs'],
6 | };
7 |
--------------------------------------------------------------------------------
/packages/create-sunar/configs/static-eslint-ts.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { es2022: true },
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
5 | ignorePatterns: ['dist', '.eslintrc.cjs'],
6 | parser: '@typescript-eslint/parser',
7 | };
8 |
--------------------------------------------------------------------------------
/packages/create-sunar/configs/static-prettier.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "endOfLine": "auto",
5 | "semi": true,
6 | "tabWidth": 4,
7 | "printWidth": 100,
8 | "useTabs": true
9 | }
10 |
--------------------------------------------------------------------------------
/packages/create-sunar/configs/static-tsup.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | entry: ['src/**/*.ts', '!src/**/*.spec.ts'],
5 | format: 'esm',
6 | clean: true,
7 | dts: false,
8 | minify: true,
9 | target: 'es2022',
10 | bundle: true,
11 | sourcemap: true,
12 | keepNames: true,
13 | skipNodeModulesBundle: true,
14 | ignoreWatch: ['**/node_modules/**', '**/.git/**'],
15 | });
16 |
--------------------------------------------------------------------------------
/packages/create-sunar/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-sunar",
3 | "version": "1.3.0",
4 | "description": "The easiest way to get started with Sunar.",
5 | "author": "tai (https://github.com/taii03)",
6 | "license": "MIT",
7 | "type": "module",
8 | "main": "dist/index.cjs",
9 | "types": "dist/index.d.ts",
10 | "module": "dist/index.js",
11 | "bin": {
12 | "create-sunar": "dist/index.js"
13 | },
14 | "files": ["dist", "configs", "templates"],
15 | "scripts": {
16 | "build": "tsup",
17 | "lint": "biome lint ./src",
18 | "format": "biome format --write .",
19 | "dev": "pnpm run build --watch"
20 | },
21 | "keywords": [
22 | "discord",
23 | "api",
24 | "bot",
25 | "sunar",
26 | "create-sunar",
27 | "cli",
28 | "client",
29 | "node",
30 | "discord.js",
31 | "typescript"
32 | ],
33 | "dependencies": {
34 | "@clack/prompts": "^0.7.0",
35 | "colorette": "^2.0.20",
36 | "fs-extra": "^11.2.0",
37 | "validate-npm-package-name": "^6.0.0"
38 | },
39 | "devDependencies": {
40 | "@biomejs/biome": "1.9.2",
41 | "@total-typescript/tsconfig": "1.0.4",
42 | "@types/fs-extra": "^11.0.4",
43 | "@types/node": "^22.7.4",
44 | "@types/validate-npm-package-name": "^4.0.2",
45 | "arg": "^5.0.2",
46 | "esbuild-plugin-version-injector": "1.2.1",
47 | "tsup": "8.3.0",
48 | "tsx": "4.19.1",
49 | "typescript": "5.6.2"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/copy-config.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path';
2 | import fs from 'fs-extra';
3 |
4 | import { DIRNAME } from '../utils/constants';
5 |
6 | export async function copyConfig(name: string, path: string) {
7 | const configPath = join(DIRNAME, '..', 'configs', name);
8 | await fs.copyFile(configPath, path);
9 | }
10 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/copy-template.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | import type { Language } from '../types';
4 | import { templates } from '../utils/templates';
5 |
6 | interface CopyTemplateOptions {
7 | language: Language;
8 | path: string;
9 | }
10 |
11 | export async function copyTemplate({ language, path }: CopyTemplateOptions) {
12 | await fs.copy(templates[language], path);
13 | }
14 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/get-dependencies.ts:
--------------------------------------------------------------------------------
1 | import type { Dependency, Features, Language } from '../types';
2 | import { DEPENDENCIES } from '../utils/dependencies';
3 |
4 | interface PackageDependencies {
5 | dependencies: Record;
6 | devDependencies?: Record;
7 | }
8 |
9 | export function getDependencies(language: Language, features: Features): PackageDependencies {
10 | const deps: Dependency[] = ['sunar', 'discord.js', 'dotenv'];
11 | const devDeps: Dependency[] = [];
12 |
13 | if (features.eslint) devDeps.push('eslint');
14 |
15 | if (language === 'javascript') {
16 | if (features.nodemon) devDeps.push('nodemon');
17 | }
18 |
19 | if (language === 'typescript') {
20 | devDeps.push('@types/node', 'typescript');
21 |
22 | if (features.tsx) devDeps.push('tsx');
23 | if (features.tsup) devDeps.push('tsup');
24 | if (features.eslint) {
25 | devDeps.push('@typescript-eslint/parser', '@typescript-eslint/eslint-plugin');
26 | }
27 | }
28 |
29 | if (features.biome) devDeps.push('@biomejs/biome');
30 | if (features.prettier) devDeps.push('prettier');
31 |
32 | const depsEntries = deps.map((dep) => [dep, DEPENDENCIES[dep]]);
33 | const devDepsEntries = devDeps.map((dep) => [dep, DEPENDENCIES[dep]]);
34 |
35 | depsEntries.sort();
36 | devDepsEntries.sort();
37 |
38 | const dependencies = Object.fromEntries(depsEntries);
39 | const devDependencies = Object.fromEntries(devDepsEntries);
40 |
41 | const devDepsLength = devDepsEntries.length;
42 |
43 | return {
44 | dependencies,
45 | devDependencies: devDepsLength > 0 ? devDependencies : undefined,
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/get-features.ts:
--------------------------------------------------------------------------------
1 | import type { Feature, Features, Language } from '../types';
2 | import { BASE_FEATURES, FEATURES, JS_FEATURES, TS_FEATURES } from '../utils/features';
3 |
4 | export function getFeaturesOptions(language: Language) {
5 | if (language === 'javascript') return [...BASE_FEATURES, ...JS_FEATURES];
6 | return [...BASE_FEATURES, ...TS_FEATURES];
7 | }
8 |
9 | export function getFeatures(features: Feature[]): Features {
10 | const entries = FEATURES.map((f) => [f, features.includes(f)]);
11 | return Object.fromEntries(entries);
12 | }
13 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/get-scripts.ts:
--------------------------------------------------------------------------------
1 | import type { Features, Language } from '../types';
2 | import { SCRIPTS } from '../utils/scripts';
3 |
4 | export function getScripts(language: Language, features: Features) {
5 | return {
6 | start: start(language, features),
7 | dev: dev(features),
8 | build: build(language, features),
9 | lint: lint(language, features),
10 | format: format(features),
11 | };
12 | }
13 |
14 | function start(language: Language, features: Features): string {
15 | if (language === 'typescript' || features.tsup) {
16 | return SCRIPTS.START.NODE_DIST;
17 | }
18 |
19 | return SCRIPTS.START.NODE;
20 | }
21 |
22 | function dev(features: Features): string | undefined {
23 | if (features.nodemon) return SCRIPTS.DEV.NODEMON;
24 | if (features.tsx) return SCRIPTS.DEV.TSX;
25 | }
26 |
27 | function build(language: Language, features: Features): string | undefined {
28 | if (language === 'javascript') return;
29 |
30 | if (features.tsup) return SCRIPTS.BUILD.TSUP;
31 |
32 | return SCRIPTS.BUILD.TSC;
33 | }
34 |
35 | function lint(language: Language, features: Features): string | undefined {
36 | const isTS = language === 'typescript';
37 |
38 | if (features.biome) return SCRIPTS.LINT.BIOME;
39 |
40 | if (features.eslint) {
41 | if (isTS) return SCRIPTS.LINT.TS_ESLINT;
42 | return SCRIPTS.LINT.JS_ESLINT;
43 | }
44 |
45 | if (isTS) return SCRIPTS.LINT.TSC;
46 | }
47 |
48 | function format(features: Features): string | undefined {
49 | if (features.biome) return SCRIPTS.FORMAT.BIOME;
50 | if (features.prettier) return SCRIPTS.FORMAT.PRETTIER;
51 | }
52 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/is-empty-dir.ts:
--------------------------------------------------------------------------------
1 | /*
2 | The original code comes from:
3 | https://github.com/vercel/next.js
4 | */
5 |
6 | import { lstatSync, readdirSync } from 'node:fs';
7 | import { join } from 'node:path';
8 | import { note } from '@clack/prompts';
9 | import { blue, redBright, underline } from 'colorette';
10 |
11 | const validFiles = [
12 | '.DS_Store',
13 | '.git',
14 | '.gitattributes',
15 | '.gitignore',
16 | '.gitlab-ci.yml',
17 | '.hg',
18 | '.hgcheck',
19 | '.hgignore',
20 | '.idea',
21 | '.npmignore',
22 | '.travis.yml',
23 | 'LICENSE',
24 | 'Thumbs.db',
25 | 'docs',
26 | 'mkdocs.yml',
27 | 'npm-debug.log',
28 | 'yarn-debug.log',
29 | 'yarn-error.log',
30 | 'yarnrc.yml',
31 | '.yarn',
32 | ];
33 |
34 | const imlRegex = /\.iml$/;
35 |
36 | export function isEmptyDir(root: string, name: string): boolean {
37 | const conflicts = readdirSync(root).filter(
38 | (file) => !(validFiles.includes(file) || imlRegex.test(file)),
39 | );
40 |
41 | if (conflicts.length <= 0) return true;
42 |
43 | const invalidFiles = conflicts.map((file) => {
44 | try {
45 | const stats = lstatSync(join(root, file));
46 | if (stats.isDirectory()) return ` ${blue(file)}/`;
47 | return ` ${file}`;
48 | } catch {
49 | return ` ${file}`;
50 | }
51 | });
52 |
53 | note(`${redBright(`The directory ${underline(name)} contains files that could conflict:`)}
54 |
55 | ${invalidFiles.join('\n')}
56 |
57 | Try using a new directory name, or remove the files listed above.`);
58 |
59 | return false;
60 | }
61 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/is-writeable.ts:
--------------------------------------------------------------------------------
1 | /*
2 | The original code comes from:
3 | https://github.com/vercel/next.js
4 | */
5 |
6 | import { W_OK } from 'node:constants';
7 | import { access } from 'node:fs/promises';
8 |
9 | export async function isWriteable(directory: string): Promise {
10 | try {
11 | await access(directory, W_OK);
12 | return true;
13 | } catch (err) {
14 | return false;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/node-version.ts:
--------------------------------------------------------------------------------
1 | export function checkNodeVersion() {
2 | const currentVersion = process.versions.node;
3 |
4 | const currentVersionMajor = currentVersion.split('.')[0];
5 | if (!currentVersionMajor) {
6 | console.error('No current major version found.');
7 | process.exit(1);
8 | }
9 |
10 | const requiredMajorVersion = Number.parseInt(currentVersionMajor, 10);
11 | const minimumMajorVersion = 18;
12 |
13 | if (requiredMajorVersion < minimumMajorVersion) {
14 | console.error(`Node.js v${currentVersion} is out of date and unsupported!`);
15 | console.error(`Please use Node.js v${minimumMajorVersion} or higher.`);
16 | process.exit(1);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/setup-features.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path';
2 |
3 | import fs from 'fs-extra';
4 |
5 | import type { Features, Language } from '../types';
6 | import { DEPENDENCIES } from '../utils/dependencies';
7 | import { copyConfig } from './copy-config';
8 |
9 | interface SetupFeaturesOptions {
10 | path: string;
11 | language: Language;
12 | features: Features;
13 | }
14 |
15 | export function setupFeatures({ path, language, features }: SetupFeaturesOptions) {
16 | const tasks: Promise[] = [];
17 |
18 | if (features.biome) {
19 | tasks.push(
20 | (async () => {
21 | const biomeJsonPath = join(path, 'biome.json');
22 | await copyConfig('static-biome.json', biomeJsonPath);
23 | const biomeJson = await fs.readJSON(biomeJsonPath);
24 | Object.assign(biomeJson, {
25 | $schema: `https://biomejs.dev/schemas/${DEPENDENCIES['@biomejs/biome']}/schema.json`,
26 | });
27 | await fs.writeJSON(biomeJsonPath, biomeJson, { spaces: 4 });
28 | })(),
29 | );
30 | }
31 |
32 | if (features.eslint) {
33 | tasks.push(
34 | (async () => {
35 | const eslintPath = join(path, '.eslintrc.cjs');
36 | const config = language === 'typescript' ? 'static-eslint-ts.cjs' : 'static-eslint-js.cjs';
37 | await copyConfig(config, eslintPath);
38 | })(),
39 | );
40 | }
41 |
42 | if (features.prettier) {
43 | tasks.push(
44 | (async () => {
45 | const prettierPath = join(path, '.prettierrc');
46 | await copyConfig('static-prettier.json', prettierPath);
47 | })(),
48 | );
49 | }
50 |
51 | if (features.tsup && language === 'typescript') {
52 | tasks.push(
53 | (async () => {
54 | const tsupPath = join(path, 'tsup.config.ts');
55 | await copyConfig('static-tsup.ts', tsupPath);
56 | })(),
57 | );
58 | }
59 |
60 | return Promise.all(tasks);
61 | }
62 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/helpers/validate-name.ts:
--------------------------------------------------------------------------------
1 | import validateProjectName from 'validate-npm-package-name';
2 |
3 | export function validateName(name: string): string[] {
4 | const validation = validateProjectName(name);
5 | if (validation.validForNewPackages) return [];
6 | return [...(validation.errors ?? []), ...(validation.warnings ?? [])];
7 | }
8 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/setup.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'node:child_process';
2 | import { mkdirSync } from 'node:fs';
3 | import { basename, dirname, join } from 'node:path';
4 |
5 | import { cancel } from '@clack/prompts';
6 | import fs from 'fs-extra';
7 |
8 | import { getDependencies } from './helpers/get-dependencies';
9 | import { getFeatures } from './helpers/get-features';
10 | import { getScripts } from './helpers/get-scripts';
11 | import { isEmptyDir } from './helpers/is-empty-dir';
12 | import { isWriteable } from './helpers/is-writeable';
13 |
14 | import { setupFeatures } from './helpers/setup-features';
15 | import type { Feature, Language } from './types';
16 |
17 | interface SetupOptions {
18 | path: string;
19 | language: Language;
20 | allowedFeatures: Feature[];
21 | }
22 |
23 | export async function setup({ path, language, allowedFeatures }: SetupOptions) {
24 | const writeable = await isWriteable(dirname(path));
25 |
26 | if (!writeable) {
27 | cancel('The bot path is not writable, check directory permissions and try again');
28 | process.exit(1);
29 | }
30 |
31 | const name = basename(path);
32 |
33 | mkdirSync(path, { recursive: true });
34 | if (!isEmptyDir(path, name)) process.exit(1);
35 |
36 | process.chdir(path);
37 |
38 | execSync('npm init -y');
39 |
40 | const packageJsonPath = join(path, 'package.json');
41 | const packageJson = await fs.readJSON(packageJsonPath);
42 |
43 | const features = getFeatures(allowedFeatures);
44 |
45 | const { dependencies, devDependencies } = getDependencies(language, features);
46 |
47 | Object.assign(packageJson, {
48 | name,
49 | description: 'An awesome Discord bot created with Sunar.',
50 | main: language === 'javascript' ? 'src/index.js' : 'dist/index.js',
51 | type: 'module',
52 | version: '0.0.0',
53 | scripts: getScripts(language, features),
54 | dependencies,
55 | devDependencies,
56 | });
57 |
58 | await fs.writeJSON(packageJsonPath, packageJson, { spaces: 4 });
59 | await setupFeatures({ path, language, features });
60 | }
61 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { DEPENDENCIES } from './utils/dependencies';
2 | import type { FEATURES } from './utils/features';
3 | import type { LANGUAGES } from './utils/templates';
4 |
5 | export type Language = (typeof LANGUAGES)[number];
6 | export type Feature = (typeof FEATURES)[number];
7 | export type Features = Record;
8 |
9 | export type Dependency = keyof typeof DEPENDENCIES;
10 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import { dirname } from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 |
4 | export const DIRNAME = dirname(fileURLToPath(import.meta.url));
5 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/utils/dependencies.ts:
--------------------------------------------------------------------------------
1 | export const DEPENDENCIES = {
2 | sunar: '0.6.0',
3 | 'discord.js': '14.16.2',
4 | dotenv: '16.4.5',
5 |
6 | '@types/node': '22.7.4',
7 | typescript: '5.6.2',
8 |
9 | tsx: '4.19.1',
10 | tsup: '8.3.0',
11 | nodemon: '3.1.7',
12 |
13 | '@biomejs/biome': '1.9.2',
14 | eslint: '8.57.0',
15 | '@typescript-eslint/parser': '7.14.1',
16 | '@typescript-eslint/eslint-plugin': '7.14.1',
17 | prettier: '3.3.3',
18 | } as const;
19 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/utils/features.ts:
--------------------------------------------------------------------------------
1 | export const FEATURES = ['biome', 'tsup', 'tsx', 'nodemon', 'prettier', 'eslint'] as const;
2 |
3 | export const BASE_FEATURES = [
4 | {
5 | label: 'biome',
6 | value: 'biome',
7 | hint: 'format, lint, and more in a fraction of a second',
8 | },
9 | {
10 | label: 'prettier',
11 | value: 'prettier',
12 | hint: 'format your project code',
13 | },
14 | {
15 | label: 'eslint',
16 | value: 'eslint',
17 | hint: 'static code analysis tool',
18 | },
19 | ] as const;
20 |
21 | export const JS_FEATURES = [
22 | {
23 | label: 'nodemon',
24 | value: 'nodemon',
25 | hint: 'monitor for any changes in your Node.js code',
26 | },
27 | ] as const;
28 |
29 | export const TS_FEATURES = [
30 | {
31 | label: 'tsup',
32 | value: 'tsup',
33 | hint: 'fast and minimalist TypeScript bundler',
34 | },
35 | {
36 | label: 'tsx',
37 | value: 'tsx',
38 | hint: 'easiest way to run TypeScript in Node.js',
39 | },
40 | ] as const;
41 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/utils/scripts.ts:
--------------------------------------------------------------------------------
1 | export const SCRIPTS = {
2 | START: {
3 | NODE: 'node src/index.js',
4 | NODE_DIST: 'node dist/index.js',
5 | },
6 | DEV: {
7 | NODEMON: 'nodemon src/index.js',
8 | TSX: 'tsx watch src/index.ts',
9 | },
10 | BUILD: {
11 | TSUP: 'tsup',
12 | TSC: 'tsc',
13 | },
14 | FORMAT: {
15 | BIOME: 'biome format --write .',
16 | PRETTIER: 'prettier --write .',
17 | },
18 | LINT: {
19 | BIOME: 'biome lint ./src',
20 | JS_ESLINT: 'eslint . --ext js --report-unused-disable-directives --max-warnings 0',
21 | TS_ESLINT: 'eslint . --ext ts --report-unused-disable-directives --max-warnings 0',
22 | TSC: 'tsc --noEmit',
23 | },
24 | } as const;
25 |
--------------------------------------------------------------------------------
/packages/create-sunar/src/utils/templates.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path';
2 | import { DIRNAME } from './constants';
3 |
4 | export const LANGUAGES = ['javascript', 'typescript'] as const;
5 |
6 | const javascript = join(DIRNAME, '..', 'templates', 'javascript');
7 | const typescript = join(DIRNAME, '..', 'templates', 'typescript');
8 |
9 | export const templates = { javascript, typescript };
10 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/javascript/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
4 | .env*
5 | !.env.example
6 |
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | .pnpm-debug.log*
14 |
15 | **/*.DS_Store
--------------------------------------------------------------------------------
/packages/create-sunar/templates/javascript/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Your Sunar Project
2 |
3 | This is a [Sunar](https://sunar.js.org) project. Get ready to build amazing things!
4 |
5 | ## 🚀 Getting Started
6 |
7 | Start the development server with your preferred package manager:
8 |
9 | ```bash
10 | npm run dev
11 | # or
12 | yarn dev
13 | # or
14 | pnpm dev
15 | # or
16 | bun dev
17 | ```
18 |
19 | ## 📚 Learn More
20 |
21 | Explore the following resources to dive deeper into Sunar:
22 |
23 | - [Sunar Documentation](https://sunar.js.org/docs): Learn about Sunar's features and API.
24 | - [Getting Started with Sunar](https://sunar.js.org/docs/getting-started): An interactive tutorial to get you up and running.
25 |
26 | ## 🤝 Join the Community
27 |
28 | Check out the [Sunar GitHub repository](https://github.com/sunarjs/sunar). Your feedback and contributions are always welcome!
29 |
30 | Happy coding! 🌟
31 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/javascript/src/commands/avatar.js:
--------------------------------------------------------------------------------
1 | import { ApplicationCommandOptionType } from 'discord.js';
2 | import { Slash, execute } from 'sunar';
3 |
4 | const slash = new Slash({
5 | name: 'avatar',
6 | description: 'Show user avatar',
7 | options: [
8 | {
9 | name: 'target',
10 | description: 'Target user',
11 | type: ApplicationCommandOptionType.User,
12 | },
13 | ],
14 | });
15 |
16 | execute(slash, (interaction) => {
17 | const user = interaction.options.getUser('target') ?? interaction.user;
18 | const avatarURL = user.displayAvatarURL({ size: 1024, forceStatic: false });
19 |
20 | interaction.reply({
21 | content: `Avatar of user **${interaction.user.username}**`,
22 | files: [avatarURL],
23 | });
24 | });
25 |
26 | export { slash };
27 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/javascript/src/index.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import { GatewayIntentBits } from 'discord.js';
4 | import { Client, load } from 'sunar';
5 |
6 | const client = new Client({
7 | intents: [
8 | GatewayIntentBits.Guilds,
9 | GatewayIntentBits.GuildMembers,
10 | GatewayIntentBits.GuildMessages,
11 | ],
12 | });
13 |
14 | await load('src/{commands,signals}/**/*.js');
15 |
16 | client.login();
17 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/javascript/src/signals/interaction-create.js:
--------------------------------------------------------------------------------
1 | import { Signal, Signals, execute } from 'sunar';
2 | import { handleInteraction } from 'sunar/handlers';
3 |
4 | const signal = new Signal(Signals.InteractionCreate);
5 |
6 | execute(signal, (interaction) => {
7 | // handle all the interactions
8 | handleInteraction(interaction);
9 |
10 | // handle specific interactions:
11 | // https://sunar.js.org/docs/guides/interactions-handling#handle-only-specific-interactions
12 | });
13 |
14 | export { signal };
15 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/javascript/src/signals/ready.js:
--------------------------------------------------------------------------------
1 | import { Signal, Signals, execute } from 'sunar';
2 | import { registerCommands } from 'sunar/registry';
3 |
4 | const signal = new Signal(Signals.ClientReady, { once: true });
5 |
6 | execute(signal, async (client) => {
7 | // this will register commands either globally or per server
8 | await registerCommands(client.application);
9 | // register commands for specific guild IDs: https://sunar.js.org/docs/guides/registering-commands/guilds
10 | // register commands globally: https://sunar.js.org/docs/guides/registering-commands/global
11 |
12 | console.info(`Bot ${client.user.tag} ready!`);
13 | });
14 |
15 | export { signal };
16 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/typescript/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
4 | .env*
5 | !.env.example
6 |
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | .pnpm-debug.log*
14 |
15 | **/*.DS_Store
--------------------------------------------------------------------------------
/packages/create-sunar/templates/typescript/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Your Sunar Project
2 |
3 | This is a [Sunar](https://sunar.js.org) project. Get ready to build amazing things!
4 |
5 | ## 🚀 Getting Started
6 |
7 | Start the development server with your preferred package manager:
8 |
9 | ```bash
10 | npm run dev
11 | # or
12 | yarn dev
13 | # or
14 | pnpm dev
15 | # or
16 | bun dev
17 | ```
18 |
19 | ## 📚 Learn More
20 |
21 | Explore the following resources to dive deeper into Sunar:
22 |
23 | - [Sunar Documentation](https://sunar.js.org/docs): Learn about Sunar's features and API.
24 | - [Getting Started with Sunar](https://sunar.js.org/docs/getting-started): An interactive tutorial to get you up and running.
25 |
26 | ## 🤝 Join the Community
27 |
28 | Check out the [Sunar GitHub repository](https://github.com/sunarjs/sunar). Your feedback and contributions are always welcome!
29 |
30 | Happy coding! 🌟
31 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/typescript/src/commands/avatar.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationCommandOptionType } from 'discord.js';
2 | import { Slash, execute } from 'sunar';
3 |
4 | const slash = new Slash({
5 | name: 'avatar',
6 | description: 'Show user avatar',
7 | options: [
8 | {
9 | name: 'target',
10 | description: 'Target user',
11 | type: ApplicationCommandOptionType.User,
12 | },
13 | ],
14 | });
15 |
16 | execute(slash, (interaction) => {
17 | const user = interaction.options.getUser('target') ?? interaction.user;
18 | const avatarURL = user.displayAvatarURL({ size: 1024, forceStatic: false });
19 |
20 | interaction.reply({
21 | content: `Avatar of user **${interaction.user.username}**`,
22 | files: [avatarURL],
23 | });
24 | });
25 |
26 | export { slash };
27 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/typescript/src/index.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import { GatewayIntentBits } from 'discord.js';
4 | import { Client, dirname, load } from 'sunar';
5 |
6 | const client = new Client({
7 | intents: [
8 | GatewayIntentBits.Guilds,
9 | GatewayIntentBits.GuildMembers,
10 | GatewayIntentBits.GuildMessages,
11 | ],
12 | });
13 |
14 | await load(`${dirname(import.meta.url)}/{commands,signals}/**/*.{js,ts}`);
15 |
16 | client.login();
17 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/typescript/src/signals/interaction-create.ts:
--------------------------------------------------------------------------------
1 | import { Signal, Signals, execute } from 'sunar';
2 | import { handleInteraction } from 'sunar/handlers';
3 |
4 | const signal = new Signal(Signals.InteractionCreate);
5 |
6 | execute(signal, (interaction) => {
7 | // handle all the interactions
8 | handleInteraction(interaction);
9 |
10 | // handle specific interactions:
11 | // https://sunar.js.org/docs/guides/interactions-handling#handle-only-specific-interactions
12 | });
13 |
14 | export { signal };
15 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/typescript/src/signals/ready.ts:
--------------------------------------------------------------------------------
1 | import { Signal, Signals, execute } from 'sunar';
2 | import { registerCommands } from 'sunar/registry';
3 |
4 | const signal = new Signal(Signals.ClientReady, { once: true });
5 |
6 | execute(signal, async (client) => {
7 | // this will register commands either globally or per server
8 | await registerCommands(client.application);
9 | // register commands for specific guild IDs: https://sunar.js.org/docs/guides/registering-commands/guilds
10 | // register commands globally: https://sunar.js.org/docs/guides/registering-commands/global
11 |
12 | console.info(`Bot ${client.user.tag} ready!`);
13 | });
14 |
15 | export { signal };
16 |
--------------------------------------------------------------------------------
/packages/create-sunar/templates/typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | /* from https://github.com/total-typescript/tsconfig - bundler/no-dom/app */
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | /* Base Options: */
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "target": "es2022",
10 | "allowJs": true,
11 | "resolveJsonModule": true,
12 | "moduleDetection": "force",
13 | "isolatedModules": true,
14 | "verbatimModuleSyntax": true,
15 | /* Strictness */
16 | "strict": true,
17 | "noUncheckedIndexedAccess": true,
18 | "noImplicitOverride": true,
19 | /* If NOT transpiling with TypeScript: */
20 | "module": "preserve",
21 | "noEmit": true,
22 | /* If your code doesn't run in the DOM: */
23 | "lib": ["es2022"]
24 | },
25 | "include": ["src"],
26 | "exclude": ["node_modules", "dist"]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/create-sunar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@total-typescript/tsconfig/bundler/no-dom/library-monorepo",
3 | "compilerOptions": {
4 | "module": "ES2022",
5 | "moduleResolution": "Bundler"
6 | },
7 | "include": ["src/**/*.ts"],
8 | "exclude": ["node_modules", "templates", "configs"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/create-sunar/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import arg from 'arg';
2 | import { defineConfig } from 'tsup';
3 |
4 | import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
5 |
6 | const args = arg({ '--watch': Boolean });
7 | const isWatch = Boolean(args['--watch']);
8 |
9 | export default defineConfig({
10 | entry: ['src/**/*.ts', '!src/**/*.spec.ts'],
11 | format: 'esm',
12 | clean: true,
13 | dts: false,
14 | target: 'es2022',
15 | bundle: true,
16 | minifyWhitespace: true,
17 | minifyIdentifiers: false,
18 | minifySyntax: false,
19 | watch: isWatch,
20 | sourcemap: true,
21 | keepNames: true,
22 | shims: true,
23 | skipNodeModulesBundle: true,
24 | esbuildPlugins: [esbuildPluginVersionInjector()],
25 | ignoreWatch: ['**/node_modules/**', '**/.git/**'],
26 | });
27 |
--------------------------------------------------------------------------------
/packages/sunar/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
4 | .env
5 | .env.development
6 |
7 | bun.lockb
8 |
--------------------------------------------------------------------------------
/packages/sunar/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "biomejs.biome",
3 | "typescript.tsdk": "node_modules\\typescript\\lib",
4 |
5 | "conventionalCommits.scopes": ["readme", "vscode", "jsdoc", "biome"],
6 | "cSpell.words": [
7 | "autocompletes",
8 | "biomejs",
9 | "commitlint",
10 | "cooldowns",
11 | "Protectable",
12 | "Repliable",
13 | "sunar",
14 | "treeshake"
15 | ],
16 | "exportall.config.folderListener": [
17 | "/src/types",
18 | "/src/stores",
19 | "/src/builders",
20 | "/src/mutators",
21 | "/src/registry",
22 | "/src/handlers",
23 | "/src/modules",
24 | "/src/utils"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/sunar/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sunar JavaScript Community
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/sunar/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
3 | "organizeImports": {
4 | "enabled": true
5 | },
6 | "linter": {
7 | "enabled": true,
8 | "rules": {
9 | "complexity": {
10 | "all": true,
11 | "noExcessiveCognitiveComplexity": "off",
12 | "noThisInStatic": "off"
13 | },
14 | "correctness": {
15 | "recommended": true
16 | },
17 | "nursery": {
18 | "all": true,
19 | "noEnum": "off",
20 | "useImportRestrictions": "off",
21 | "useConsistentMemberAccessibility": "off"
22 | },
23 | "performance": {
24 | "all": true,
25 | "noBarrelFile": "off",
26 | "noReExportAll": "off"
27 | },
28 | "security": {
29 | "all": true
30 | },
31 | "style": {
32 | "recommended": true
33 | },
34 | "suspicious": {
35 | "all": true,
36 | "noEmptyBlockStatements": "off",
37 | "noExplicitAny": "off"
38 | }
39 | }
40 | },
41 | "formatter": {
42 | "enabled": true,
43 | "indentStyle": "tab",
44 | "indentWidth": 2,
45 | "lineWidth": 80
46 | },
47 | "javascript": {
48 | "formatter": {
49 | "enabled": true,
50 | "quoteStyle": "single",
51 | "indentWidth": 2,
52 | "lineWidth": 120,
53 | "semicolons": "always",
54 | "trailingCommas": "all"
55 | }
56 | },
57 | "files": {
58 | "include": ["**/*"],
59 | "ignore": ["node_modules", "dist"]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/sunar/commitlint.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | extends: ['@commitlint/config-conventional'],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/autocomplete.ts:
--------------------------------------------------------------------------------
1 | import type { AutocompleteFocusedOption, AutocompleteInteraction, Awaitable } from 'discord.js';
2 |
3 | import type { Protector } from '.';
4 | import { UNHANDLED_SYMBOL } from '../symbols';
5 | import type { Builder } from '../types';
6 | import { Builders } from '../utils';
7 |
8 | export interface AutocompleteOptions {
9 | /** The name of the command option that has autocomplete enabled. */
10 | name: string | RegExp;
11 | /** Filters the autocomplete execution by the command name. */
12 | commandName?: string | RegExp;
13 | }
14 |
15 | /**
16 | * Autocomplete commands enhance the user experience by providing suggestions while the user is typing. They are particularly useful for commands with multiple options or extensive inputs.
17 | *
18 | * @see https://sunar.js.org/docs/builders/autocomplete
19 | */
20 | export class Autocomplete implements Omit {
21 | public readonly type = Builders.Autocomplete;
22 | public readonly options: AutocompleteOptions;
23 |
24 | public protectors: Protector<{ commands: 'autocomplete'[] }>[] = [];
25 | public execute: (interaction: AutocompleteInteraction, option: AutocompleteFocusedOption) => Awaitable =
26 | () => UNHANDLED_SYMBOL;
27 |
28 | constructor(options: AutocompleteOptions) {
29 | this.options = options;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/button.ts:
--------------------------------------------------------------------------------
1 | import type { Awaitable, ButtonInteraction } from 'discord.js';
2 |
3 | import type { Protector } from '.';
4 | import { UNHANDLED_SYMBOL } from '../symbols';
5 | import type { Builder, CooldownResolvable } from '../types';
6 | import { Builders } from '../utils';
7 |
8 | export interface ButtonOptions {
9 | /** The button custom ID to target. */
10 | id: string | RegExp;
11 | }
12 |
13 | export interface ButtonConfig {
14 | cooldown?: CooldownResolvable;
15 | }
16 |
17 | /**
18 | * Buttons are interactive elements users can click to trigger specific actions. They are ideal for creating interactive messages, such as confirmation prompts or menu navigation.
19 | *
20 | * @see https://sunar.js.org/docs/builders/button
21 | */
22 | export class Button implements Builder {
23 | public readonly type = Builders.Button;
24 | public readonly options: ButtonOptions;
25 |
26 | public config: ButtonConfig = {};
27 | public protectors: Protector<{ components: 'button'[] }>[] = [];
28 | public execute: (interaction: ButtonInteraction) => Awaitable = () => UNHANDLED_SYMBOL;
29 |
30 | constructor(options: ButtonOptions) {
31 | this.options = options;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/contextMenu.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ApplicationCommandType,
3 | Awaitable,
4 | ContextMenuCommandInteraction,
5 | MessageApplicationCommandData,
6 | MessageContextMenuCommandInteraction,
7 | UserApplicationCommandData,
8 | UserContextMenuCommandInteraction,
9 | } from 'discord.js';
10 |
11 | import type { Protector } from '.';
12 | import { UNHANDLED_SYMBOL } from '../symbols';
13 | import type { Builder, CommandConfig } from '../types';
14 | import { Builders } from '../utils';
15 |
16 | export type ContextMenuData = MessageApplicationCommandData | UserApplicationCommandData;
17 |
18 | export interface ContextMenuConfig extends CommandConfig {}
19 |
20 | /**
21 | * Context menu commands are available directly in the right-click context menu for users or messages. These commands are convenient for quick actions without needing to type a command.
22 | *
23 | * @see https://sunar.js.org/docs/builders/context-menu
24 | */
25 | export class ContextMenu implements Builder {
26 | public readonly type = Builders.ContextMenu;
27 | public readonly data: TData;
28 |
29 | public config: ContextMenuConfig = {};
30 | public protectors: Protector<{ commands: 'contextMenu'[] }>[] = [];
31 | public execute: (...args: ContextMenuArgs) => Awaitable = () => UNHANDLED_SYMBOL;
32 |
33 | constructor(data: TData) {
34 | this.data = data;
35 | }
36 | }
37 |
38 | export type ContextMenuArgs = [
39 | interaction: TData['type'] extends ApplicationCommandType.Message
40 | ? MessageContextMenuCommandInteraction
41 | : TData['type'] extends ApplicationCommandType.User
42 | ? UserContextMenuCommandInteraction
43 | : ContextMenuCommandInteraction,
44 | ];
45 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/group.ts:
--------------------------------------------------------------------------------
1 | import type { Awaitable, ChatInputCommandInteraction } from 'discord.js';
2 |
3 | import type { Protector } from '.';
4 | import { UNHANDLED_SYMBOL } from '../symbols';
5 | import type { Builder, CooldownProp } from '../types';
6 | import { Builders } from '../utils';
7 |
8 | export interface GroupConfig extends CooldownProp {}
9 |
10 | /**
11 | * The Group class handles slash commands with subcommands, allowing for structured and efficient
12 | * management of hierarchical commands under a single root.
13 | *
14 | * @see https://sunar.js.org/docs/builders/group
15 | */
16 | export class Group implements Builder {
17 | public readonly type = Builders.Group;
18 |
19 | public root: string;
20 | public parent: string;
21 | public sub?: string;
22 |
23 | public config: GroupConfig = {};
24 | public protectors: Protector<{ commands: 'slash'[] }>[] = [];
25 | public execute: (interaction: ChatInputCommandInteraction) => Awaitable = () => UNHANDLED_SYMBOL;
26 |
27 | constructor(root: string, parent: string, sub?: string) {
28 | this.root = root;
29 | this.parent = parent;
30 | this.sub = sub;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/index.ts:
--------------------------------------------------------------------------------
1 | export * from './autocomplete';
2 | export * from './button';
3 | export * from './contextMenu';
4 | export * from './group';
5 | export * from './modal';
6 | export * from './protector';
7 | export * from './selectMenu';
8 | export * from './signal';
9 | export * from './slash';
10 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/modal.ts:
--------------------------------------------------------------------------------
1 | import type { Awaitable, ModalSubmitInteraction } from 'discord.js';
2 |
3 | import type { Protector } from '.';
4 | import { UNHANDLED_SYMBOL } from '../symbols';
5 | import type { Builder, CooldownResolvable } from '../types';
6 | import { Builders } from '../utils';
7 |
8 | export interface ModalOptions {
9 | /** The modal custom ID to target. */
10 | id: string | RegExp;
11 | }
12 |
13 | export interface ModalConfig {
14 | cooldown?: CooldownResolvable;
15 | }
16 |
17 | /**
18 | * Modals are popup forms that can collect detailed user input. They are particularly useful for complex interactions that require multiple fields or steps.
19 | *
20 | * @see https://sunar.js.org/docs/builders/modal
21 | */
22 | export class Modal implements Builder {
23 | public readonly type = Builders.Modal;
24 | public readonly options: ModalOptions;
25 |
26 | public config: ModalConfig = {};
27 | public protectors: Protector<{ components: 'modal'[] }>[] = [];
28 | public execute: (interaction: ModalSubmitInteraction) => Awaitable = () => UNHANDLED_SYMBOL;
29 |
30 | constructor(options: ModalOptions) {
31 | this.options = options;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/selectMenu.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AnySelectMenuInteraction,
3 | Awaitable,
4 | ChannelSelectMenuInteraction,
5 | ComponentType,
6 | MentionableSelectMenuInteraction,
7 | RoleSelectMenuInteraction,
8 | SelectMenuType,
9 | StringSelectMenuInteraction,
10 | UserSelectMenuInteraction,
11 | } from 'discord.js';
12 |
13 | import type { Protector } from '.';
14 | import { UNHANDLED_SYMBOL } from '../symbols';
15 | import type { Builder, CooldownResolvable } from '../types';
16 | import { Builders } from '../utils';
17 |
18 | export interface SelectMenuOptions {
19 | /** The select menu custom ID to target. */
20 | id: string | RegExp;
21 | /** The type of select menu to target. */
22 | type: SelectMenuType;
23 | }
24 |
25 | export interface SelectMenuConfig {
26 | cooldown?: CooldownResolvable;
27 | }
28 |
29 | /**
30 | * Select menus allow users to choose from a list of options. They are useful for forms, surveys, or any scenario where the user needs to make a selection from multiple choices.
31 | *
32 | * @see https://sunar.js.org/docs/builders/select-menu
33 | */
34 | export class SelectMenu implements Builder {
35 | public readonly type = Builders.SelectMenu;
36 | public readonly options: TOptions;
37 |
38 | public config: SelectMenuConfig = {};
39 | public protectors: Protector<{ components: 'selectMenu'[] }>[] = [];
40 | public execute: (...args: SelectMenuArgs) => Awaitable = () => UNHANDLED_SYMBOL;
41 |
42 | constructor(options: TOptions) {
43 | this.options = options;
44 | }
45 | }
46 |
47 | export type SelectMenuArgs = [
48 | interaction: TOptions['type'] extends ComponentType.ChannelSelect
49 | ? ChannelSelectMenuInteraction
50 | : TOptions['type'] extends ComponentType.MentionableSelect
51 | ? MentionableSelectMenuInteraction
52 | : TOptions['type'] extends ComponentType.RoleSelect
53 | ? RoleSelectMenuInteraction
54 | : TOptions['type'] extends ComponentType.StringSelect
55 | ? StringSelectMenuInteraction
56 | : TOptions['type'] extends ComponentType.UserSelect
57 | ? UserSelectMenuInteraction
58 | : AnySelectMenuInteraction,
59 | ];
60 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/signal.ts:
--------------------------------------------------------------------------------
1 | import type { Awaitable, ClientEvents } from 'discord.js';
2 |
3 | import type { Protector } from '.';
4 | import { UNHANDLED_SYMBOL } from '../symbols';
5 | import type { Builder } from '../types';
6 | import { Builders } from '../utils';
7 |
8 | type SignalName = keyof ClientEvents;
9 |
10 | export interface SignalOptions {
11 | /** If the signal only has to be emitted once. */
12 | once?: boolean;
13 | }
14 |
15 | /**
16 | * Signals in Sunar correspond to events in discord.js. They allow you to handle various actions and responses that occur within your Discord bot, such as messages being sent, users joining or leaving, and more.
17 | *
18 | * @see https://sunar.js.org/docs/builders/signal
19 | * @see https://sunar.js.org/docs/guides/working-with-signals
20 | */
21 | export class Signal implements Pick {
22 | public readonly type = Builders.Signal;
23 | public readonly name: TName;
24 | public readonly options: SignalOptions;
25 |
26 | public protectors: Protector<{ signals: TName[] }>[] = [];
27 | public execute: (...args: ClientEvents[TName]) => Awaitable = () => UNHANDLED_SYMBOL;
28 |
29 | constructor(name: TName, options: SignalOptions = {}) {
30 | this.name = name;
31 | this.options = options;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/sunar/src/builders/slash.ts:
--------------------------------------------------------------------------------
1 | import type { Awaitable, ChatInputApplicationCommandData, ChatInputCommandInteraction } from 'discord.js';
2 |
3 | import type { Protector } from '.';
4 | import { UNHANDLED_SYMBOL } from '../symbols';
5 | import type { Builder, CommandConfig } from '../types';
6 | import { Builders } from '../utils';
7 |
8 | export interface SlashConfig extends CommandConfig {}
9 |
10 | /**
11 | * Slash commands are one of the primary ways users interact with bots. They provide a structured way for users to issue commands directly within the chat interface.
12 | *
13 | * @see https://sunar.js.org/docs/builders/slash
14 | */
15 | export class Slash implements Builder {
16 | public readonly type = Builders.Slash;
17 | public readonly data: ChatInputApplicationCommandData;
18 |
19 | public config: SlashConfig = {};
20 | public protectors: Protector<{ commands: 'slash'[] }>[] = [];
21 | public execute: (interaction: ChatInputCommandInteraction) => Awaitable = () => UNHANDLED_SYMBOL;
22 |
23 | constructor(data: ChatInputApplicationCommandData) {
24 | this.data = data;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/sunar/src/client.ts:
--------------------------------------------------------------------------------
1 | import { type ClientOptions, Client as DClient } from 'discord.js';
2 |
3 | import { handleSignals } from './handlers';
4 | import { context } from './stores';
5 | import type { SunarSignals } from './types';
6 |
7 | export class Client extends DClient {
8 | public constructor(options: ClientOptions) {
9 | super(options);
10 |
11 | context.client = this;
12 | }
13 |
14 | public override login(token?: string): Promise {
15 | handleSignals();
16 | return super.login(token);
17 | }
18 | }
19 |
20 | declare module 'discord.js' {
21 | interface ClientEvents extends SunarSignals {}
22 | }
23 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/autocomplete/autocomplete.ts:
--------------------------------------------------------------------------------
1 | import type { AutocompleteInteraction } from 'discord.js';
2 | import { autocompletes } from '../../stores';
3 | import { handleProtectors } from '../protectors';
4 |
5 | /**
6 | * Handle an autocomplete interaction.
7 | * @param interaction The autocomplete interaction to handle
8 | *
9 | * @see https://sunar.js.org/docs/guides/interactions-handling
10 | */
11 | export async function handleAutocomplete(interaction: AutocompleteInteraction) {
12 | const focused = interaction.options.getFocused(true);
13 |
14 | const command = autocompletes.find(({ options }) => {
15 | if (options.name instanceof RegExp) return options.name.test(focused.name);
16 | if (options.commandName instanceof RegExp) return options.commandName.test(interaction.commandName);
17 | if (options.commandName && options.commandName !== interaction.commandName) return false;
18 | return options.name === focused.name;
19 | });
20 |
21 | if (!command) return;
22 |
23 | if (typeof command.execute !== 'function') return;
24 |
25 | const canContinue = await handleProtectors({ protectors: command.protectors, data: interaction });
26 | if (!canContinue) return;
27 |
28 | const result = await command.execute(interaction, focused);
29 |
30 | if (!result) return;
31 |
32 | // TODO: handle the result of the command execution
33 | }
34 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/autocomplete/index.ts:
--------------------------------------------------------------------------------
1 | export * from './autocomplete';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/button/button.ts:
--------------------------------------------------------------------------------
1 | import type { ButtonInteraction } from 'discord.js';
2 |
3 | import { handleCooldown } from '..';
4 | import { buttons } from '../../stores';
5 | import { handleProtectors } from '../protectors';
6 |
7 | /**
8 | * Handle a button interaction.
9 | * @param interaction The button interaction to handle
10 | *
11 | * @see https://sunar.js.org/docs/guides/interactions-handling
12 | */
13 | export async function handleButton(interaction: ButtonInteraction) {
14 | const component = buttons.find(({ options }) => {
15 | if (options.id instanceof RegExp) return options.id.test(interaction.customId);
16 | return options.id === interaction.customId;
17 | });
18 |
19 | if (!component) return;
20 |
21 | const onCooldown = handleCooldown(interaction, component);
22 | if (onCooldown) return;
23 |
24 | if (typeof component.execute !== 'function') return;
25 |
26 | const canContinue = await handleProtectors({ protectors: component.protectors, data: interaction });
27 | if (!canContinue) return;
28 |
29 | const result = await component.execute(interaction);
30 |
31 | if (!result) return;
32 |
33 | // TODO: handle the result of the component execution
34 | }
35 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/button/index.ts:
--------------------------------------------------------------------------------
1 | export * from './button';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/contextMenu/contextMenu.ts:
--------------------------------------------------------------------------------
1 | import type { MessageContextMenuCommandInteraction, UserContextMenuCommandInteraction } from 'discord.js';
2 | import { handleCooldown } from '..';
3 | import { contextMenuCommands } from '../../stores';
4 | import { handleProtectors } from '../protectors';
5 |
6 | /**
7 | * Handle a context menu interaction.
8 | * @param interaction The context menu interaction to handle
9 | *
10 | * @see https://sunar.js.org/docs/guides/interactions-handling
11 | */
12 | export async function handleContextMenu(
13 | interaction: UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction,
14 | ) {
15 | const command = contextMenuCommands.get(interaction.commandName);
16 |
17 | if (!command) return;
18 |
19 | const onCooldown = handleCooldown(interaction, command);
20 | if (onCooldown) return;
21 |
22 | if (typeof command.execute !== 'function') return;
23 |
24 | const canContinue = await handleProtectors({ protectors: command.protectors, data: interaction });
25 | if (!canContinue) return;
26 |
27 | const result = await command.execute(interaction);
28 |
29 | if (!result) return;
30 |
31 | // TODO: handle the result of the command execution
32 | }
33 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/contextMenu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './contextMenu';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/cooldown/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cooldown';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/group/group.ts:
--------------------------------------------------------------------------------
1 | import type { ChatInputCommandInteraction } from 'discord.js';
2 |
3 | import type { Slash } from '../../builders';
4 | import { groups } from '../../stores';
5 | import { getGroupStoreKey } from '../../utils';
6 | import { handleCooldown } from '../cooldown';
7 | import { handleProtectors } from '../protectors';
8 |
9 | export interface HandleGroupOptions {
10 | parent: string | null;
11 | sub: string | null;
12 | }
13 |
14 | export async function handleSubcommands(
15 | command: Slash,
16 | interaction: ChatInputCommandInteraction,
17 | { parent, sub }: HandleGroupOptions,
18 | ) {
19 | const root = interaction.commandName;
20 |
21 | const groupStoreKey = getGroupStoreKey(root, parent, sub);
22 |
23 | const group = groups.get(groupStoreKey);
24 | if (!group) return;
25 |
26 | const cooldownBuilder = group.config.cooldown ? group : command;
27 |
28 | const onCooldown = handleCooldown(interaction, cooldownBuilder);
29 | if (onCooldown) return;
30 |
31 | const protectors = command.protectors.concat(group.protectors);
32 |
33 | const canContinue = await handleProtectors({ protectors, data: interaction });
34 | if (!canContinue) return;
35 |
36 | typeof group.execute !== 'function' && console.log('execute not function');
37 | if (typeof group.execute !== 'function') return;
38 |
39 | const result = await group.execute(interaction);
40 |
41 | if (!result) return;
42 | }
43 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/group/index.ts:
--------------------------------------------------------------------------------
1 | export * from './group';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './autocomplete';
2 | export * from './button';
3 | export * from './contextMenu';
4 | export * from './cooldown';
5 | export * from './interaction';
6 | export * from './modal';
7 | export * from './protectors';
8 | export * from './selectMenu';
9 | export * from './signals';
10 | export * from './slash';
11 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/interaction/index.ts:
--------------------------------------------------------------------------------
1 | export * from './interaction';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/interaction/interaction.ts:
--------------------------------------------------------------------------------
1 | import type { Interaction } from 'discord.js';
2 |
3 | import { handleAutocomplete, handleModal, handleSelectMenu, handleSlash } from '..';
4 | import { handleButton } from '../button';
5 | import { handleContextMenu } from '../contextMenu';
6 |
7 | /**
8 | * Handle all the interactions supported by Sunar.
9 | * @param interaction The interaction to handle
10 | *
11 | * @see https://sunar.js.org/docs/guides/interactions-handling
12 | */
13 | export async function handleInteraction(interaction: Interaction) {
14 | if (interaction.isChatInputCommand()) await handleSlash(interaction);
15 | if (interaction.isContextMenuCommand()) await handleContextMenu(interaction);
16 | if (interaction.isModalSubmit()) await handleModal(interaction);
17 | if (interaction.isButton()) await handleButton(interaction);
18 | if (interaction.isAnySelectMenu()) await handleSelectMenu(interaction);
19 | if (interaction.isAutocomplete()) await handleAutocomplete(interaction);
20 | }
21 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/modal/index.ts:
--------------------------------------------------------------------------------
1 | export * from './modal';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/modal/modal.ts:
--------------------------------------------------------------------------------
1 | import type { ModalSubmitInteraction } from 'discord.js';
2 | import { handleCooldown } from '..';
3 | import { modals } from '../../stores';
4 | import { handleProtectors } from '../protectors';
5 |
6 | /**
7 | * Handle a modal interaction.
8 | * @param interaction The modal interaction to handle
9 | *
10 | * @see https://sunar.js.org/docs/guides/interactions-handling
11 | */
12 | export async function handleModal(interaction: ModalSubmitInteraction) {
13 | const component = modals.find(({ options }) => {
14 | if (options.id instanceof RegExp) return options.id.test(interaction.customId);
15 | return options.id === interaction.customId;
16 | });
17 |
18 | if (!component) return;
19 |
20 | const onCooldown = handleCooldown(interaction, component);
21 | if (onCooldown) return;
22 |
23 | if (typeof component.execute !== 'function') return;
24 |
25 | const canContinue = await handleProtectors({ protectors: component.protectors, data: interaction });
26 | if (!canContinue) return;
27 |
28 | const result = await component.execute(interaction);
29 |
30 | if (!result) return;
31 |
32 | // TODO: handle the result of the component execution
33 | }
34 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/protectors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './protectors';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/protectors/protectors.ts:
--------------------------------------------------------------------------------
1 | import type { Protector, ProtectorExecuteData } from '../../builders';
2 | import { PROTECTOR_NEXT_SYMBOL, UNHANDLED_SYMBOL } from '../../symbols';
3 |
4 | export interface HandleProtectorsOptions {
5 | protectors?: TProtectors;
6 | data: ProtectorExecuteData;
7 | }
8 |
9 | export async function handleProtectors({
10 | protectors,
11 | data,
12 | }: HandleProtectorsOptions): Promise {
13 | if (!protectors || protectors.length <= 0) return true;
14 |
15 | for (const protector of protectors) {
16 | if (typeof protector.execute !== 'function') continue;
17 |
18 | // FIXME: Improve types, "never" should not be used here
19 | const result = await protector.execute(data as never, () => PROTECTOR_NEXT_SYMBOL);
20 |
21 | if (result === UNHANDLED_SYMBOL) continue;
22 | if (result !== PROTECTOR_NEXT_SYMBOL) return false;
23 | }
24 |
25 | return true;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/selectMenu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './selectMenu';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/selectMenu/selectMenu.ts:
--------------------------------------------------------------------------------
1 | import type { AnySelectMenuInteraction } from 'discord.js';
2 | import { handleCooldown } from '..';
3 | import { selectMenus } from '../../stores';
4 | import { handleProtectors } from '../protectors';
5 |
6 | /**
7 | * Handle a select menu interaction.
8 | * @param interaction The select menu interaction to handle
9 | *
10 | * @see https://sunar.js.org/docs/guides/interactions-handling
11 | */
12 | export async function handleSelectMenu(interaction: AnySelectMenuInteraction) {
13 | const component = selectMenus.find(({ options }) => {
14 | if (options.type !== interaction.componentType) return false;
15 | if (options.id instanceof RegExp) return options.id.test(interaction.customId);
16 | return options.id === interaction.customId;
17 | });
18 |
19 | if (!component) return;
20 |
21 | const onCooldown = handleCooldown(interaction, component);
22 | if (onCooldown) return;
23 |
24 | if (typeof component.execute !== 'function') return;
25 |
26 | const canContinue = await handleProtectors({ protectors: component.protectors, data: interaction });
27 | if (!canContinue) return;
28 |
29 | const result = await component.execute(interaction);
30 |
31 | if (!result) return;
32 |
33 | // TODO: handle the result of the component execution
34 | }
35 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/signals/index.ts:
--------------------------------------------------------------------------------
1 | export * from './signals';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/signals/signals.ts:
--------------------------------------------------------------------------------
1 | import { context, signals } from '../../stores';
2 | import { handleProtectors } from '../protectors';
3 |
4 | /** Handle all the signals. This is used by Sunar internally. */
5 | export function handleSignals() {
6 | for (const signal of signals.values()) {
7 | if (!signal.execute) return;
8 |
9 | const method = signal.options.once ? 'once' : 'on';
10 |
11 | context.client[method](signal.name, async (...args) => {
12 | if (!signal.execute) return;
13 |
14 | const canContinue = await handleProtectors({ protectors: signal.protectors, data: args });
15 | if (!canContinue) return;
16 |
17 | signal.execute(...args);
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/slash/index.ts:
--------------------------------------------------------------------------------
1 | export * from './slash';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/handlers/slash/slash.ts:
--------------------------------------------------------------------------------
1 | import type { ChatInputCommandInteraction } from 'discord.js';
2 |
3 | import { handleCooldown } from '..';
4 | import { slashCommands } from '../../stores';
5 | import { handleSubcommands } from '../group';
6 | import { handleProtectors } from '../protectors';
7 |
8 | /**
9 | * Handle a chat input interaction.
10 | * @param interaction The chat input interaction to handle
11 | *
12 | * @see https://sunar.js.org/docs/guides/interactions-handling
13 | */
14 | export async function handleSlash(interaction: ChatInputCommandInteraction) {
15 | const command = slashCommands.get(interaction.commandName);
16 | if (!command) return;
17 |
18 | const parent = interaction.options.getSubcommandGroup(false);
19 | const sub = interaction.options.getSubcommand(false);
20 |
21 | if (parent || sub) return handleSubcommands(command, interaction, { parent, sub });
22 |
23 | const onCooldown = handleCooldown(interaction, command);
24 | if (onCooldown) return;
25 |
26 | const canContinue = await handleProtectors({ protectors: command.protectors, data: interaction });
27 | if (!canContinue) return;
28 |
29 | if (typeof command.execute !== 'function') return;
30 |
31 | const result = await command.execute(interaction);
32 |
33 | if (!result) return;
34 |
35 | // TODO: handle the result of the command execution
36 | }
37 |
--------------------------------------------------------------------------------
/packages/sunar/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './builders';
2 | export * from './client';
3 | export * from './types';
4 | export * from './mutators';
5 | export * from './modules';
6 |
7 | export * from './utils/constants';
8 | export * from './utils/enums';
9 |
10 | export const version: string = '[VI]{{inject}}[/VI]';
11 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/dirname/dirname.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 |
4 | export function dirname(url: string): string {
5 | return path.dirname(fileURLToPath(url));
6 | }
7 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/dirname/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dirname';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/index.ts:
--------------------------------------------------------------------------------
1 | export * from './load';
2 | export * from './resolve';
3 | export * from './store';
4 | export * from './dirname';
5 | export * from './isESM';
6 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/isESM/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isESM';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/isESM/isESM.ts:
--------------------------------------------------------------------------------
1 | export function isESM(): boolean {
2 | return !!import.meta.url;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/load/index.ts:
--------------------------------------------------------------------------------
1 | export * from './load';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/load/load.spec.ts:
--------------------------------------------------------------------------------
1 | import { load } from './load';
2 |
3 | import { Signal, Slash } from '../../builders';
4 | import { signals, slashCommands } from '../../stores';
5 |
6 | beforeEach(() => {
7 | signals.clear();
8 | slashCommands.clear();
9 | });
10 |
11 | describe('load()', () => {
12 | it('should return undefined when loading files', async () => {
13 | const result = await load('src/vitest/{commands,signals}/**/*.ts');
14 |
15 | expect(result).toBeUndefined();
16 | });
17 |
18 | it('should store and retrieve commands and signals correctly', async () => {
19 | await load('src/vitest/{commands,signals}/**/*.ts');
20 |
21 | const ping = slashCommands.get('ping');
22 |
23 | expect(ping).toBeInstanceOf(Slash);
24 | expect(ping?.execute).toBeTypeOf('function');
25 |
26 | expect(signals.size).toBeGreaterThan(0);
27 | });
28 |
29 | it('should ignore files with underscore prefix', async () => {
30 | await load('src/vitest/{commands,signals}/**/*.ts', {
31 | ignore: 'src/**/_*.ts',
32 | });
33 |
34 | const ignored = slashCommands.get('ignored');
35 |
36 | expect(ignored).toBeUndefined();
37 | });
38 |
39 | it('should handle empty patterns gracefully', async () => {
40 | await load([]);
41 |
42 | expect(slashCommands.size).toBe(0);
43 | expect(signals.size).toBe(0);
44 | });
45 |
46 | it('should load modules with multiple patterns', async () => {
47 | await load(['src/vitest/commands/**/*.ts', 'src/vitest/signals/**/*.ts']);
48 |
49 | const command = slashCommands.at(0);
50 | const signal = signals.at(0);
51 |
52 | expect(command).toBeInstanceOf(Slash);
53 | expect(signal).toBeInstanceOf(Signal);
54 | });
55 |
56 | it('should handle patterns that match no files', async () => {
57 | await load('src/invalid-path-test/**/*.ts');
58 |
59 | expect(slashCommands.size).toBe(0);
60 | expect(signals.size).toBe(0);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/load/load.ts:
--------------------------------------------------------------------------------
1 | import type { GlobOptions } from 'glob';
2 | import { resolve, storeModules } from '..';
3 |
4 | /**
5 | * Resolve and store all the sunar modules.
6 | *
7 | * @param patterns The glob patterns to load
8 | * @param options The glob options
9 | *
10 | * @see https://sunar.js.org/docs/guides/load-modules
11 | */
12 | export async function load(patterns: string | string[], options?: GlobOptions): Promise {
13 | const modules = await resolve(patterns, options);
14 |
15 | storeModules(modules);
16 | }
17 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/resolve/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resolve';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/resolve/resolve.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'node:path';
2 |
3 | import { type GlobOptions, glob } from 'glob';
4 |
5 | /**
6 | * Import all files that match the patterns.
7 | * @param patterns The glob patterns to resolve
8 | * @param options The glob options
9 | */
10 | export async function resolve(patterns: string | string[], options: GlobOptions = {}): Promise {
11 | const paths = await glob(patterns, options);
12 |
13 | const imports = paths.map((path) => {
14 | const absolute = join(process.cwd(), path.toString());
15 | return import(`file://${absolute}`);
16 | });
17 |
18 | return Promise.all(imports);
19 | }
20 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './store';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/modules/store/store.ts:
--------------------------------------------------------------------------------
1 | import {
2 | autocompletes,
3 | buttons,
4 | contextMenuCommands,
5 | groups,
6 | modals,
7 | selectMenus,
8 | signals,
9 | slashCommands,
10 | } from '../../stores';
11 | import {
12 | getGroupStoreKey,
13 | isAutocompleteBuilder,
14 | isButtonBuilder,
15 | isContextMenuBuilder,
16 | isGroupBuilder,
17 | isModalBuilder,
18 | isObject,
19 | isSelectMenuBuilder,
20 | isSignalBuilder,
21 | isSlashBuilder,
22 | } from '../../utils';
23 |
24 | /**
25 | * Store all the builders in the sunar collections.
26 | * @param modules The modules to store
27 | */
28 | export function storeModules(modules: unknown[]) {
29 | for (const module of modules) {
30 | if (!isObject(module)) return;
31 |
32 | const values = Object.values(module);
33 |
34 | for (const value of values) {
35 | if (isSignalBuilder(value)) signals.set(Symbol(), value);
36 | if (isSlashBuilder(value)) slashCommands.set(value.data.name, value);
37 | if (isGroupBuilder(value)) groups.set(getGroupStoreKey(value), value);
38 | if (isContextMenuBuilder(value)) contextMenuCommands.set(value.data.name, value);
39 | if (isButtonBuilder(value)) buttons.set(Symbol(), value);
40 | if (isModalBuilder(value)) modals.set(Symbol(), value);
41 | if (isSelectMenuBuilder(value)) selectMenus.set(Symbol(), value);
42 | if (isAutocompleteBuilder(value)) autocompletes.set(Symbol(), value);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/config/config.spec.ts:
--------------------------------------------------------------------------------
1 | import { Builders } from '../../utils';
2 | import { type ConfigurableBuilder, config } from './config';
3 |
4 | describe('config()', () => {
5 | it('should apply configuration to a builder', () => {
6 | const builder: ConfigurableBuilder = {
7 | type: Builders.Slash,
8 | config: { name: 'testCommand', description: 'Test command' },
9 | };
10 |
11 | const newConfig = { name: 'updatedCommand', description: 'Updated test command' };
12 |
13 | config(builder, newConfig);
14 |
15 | expect(builder.config).toEqual(newConfig);
16 | });
17 |
18 | it('should handle undefined configuration gracefully', () => {
19 | const builder: ConfigurableBuilder = {
20 | type: Builders.Button,
21 | config: { name: 'testCommand', description: 'Test command' },
22 | };
23 |
24 | // @ts-expect-error
25 | config(builder, undefined);
26 |
27 | expect(builder.config).toBeDefined();
28 | expect(builder.config).toEqual({ name: 'testCommand', description: 'Test command' });
29 | });
30 |
31 | it('should not apply configuration if config is not an object', () => {
32 | const builder: ConfigurableBuilder = {
33 | type: Builders.SelectMenu,
34 | config: { name: 'testCommand', description: 'Test command' },
35 | };
36 |
37 | const invalidConfig = 'invalid';
38 |
39 | // @ts-expect-error
40 | config(builder, invalidConfig);
41 |
42 | expect(builder.config).toEqual({ name: 'testCommand', description: 'Test command' });
43 | });
44 |
45 | it('should not modify builder if config is not an object', () => {
46 | const builder: ConfigurableBuilder = {
47 | type: Builders.ContextMenu,
48 | config: { name: 'testCommand', description: 'Test command' },
49 | };
50 |
51 | const invalidConfig = 'invalid';
52 |
53 | // @ts-expect-error
54 | config(builder, invalidConfig);
55 |
56 | expect(builder).toEqual({
57 | type: Builders.ContextMenu,
58 | config: { name: 'testCommand', description: 'Test command' },
59 | });
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/config/config.ts:
--------------------------------------------------------------------------------
1 | import type { Builder } from '../../types';
2 | import { isObject } from '../../utils';
3 |
4 | export type ConfigurableBuilder = Pick;
5 |
6 | /**
7 | * Applies the specified configuration to a builder.
8 | *
9 | * @param builder The builder to mutate
10 | * @param config The configuration to apply
11 | *
12 | * @see https://sunar.js.org/docs/mutators/config
13 | * @see https://sunar.js.org/docs/guides/implementing-cooldowns
14 | * @see https://sunar.js.org/docs/guides/registering-commands/dynamic#specific-guilds-ids-commands
15 | */
16 | export function config(builder: TBuilder, config: TBuilder['config']): void {
17 | if (!isObject(config)) return;
18 | builder.config = config;
19 | }
20 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/execute/execute.spec.ts:
--------------------------------------------------------------------------------
1 | import { Builders } from '../../utils';
2 | import { type ExecutableBuilder, execute } from './execute';
3 |
4 | describe('execute()', () => {
5 | it('should set the execute function on the builder', () => {
6 | const builder: ExecutableBuilder = {
7 | type: Builders.Slash,
8 | execute: () => {},
9 | };
10 |
11 | const newExecute = () => 'executed';
12 |
13 | execute(builder, newExecute);
14 |
15 | expect(builder.execute).toBe(newExecute);
16 | });
17 |
18 | it('should not set the execute function if it is not a function', () => {
19 | const builder: ExecutableBuilder = {
20 | type: Builders.Button,
21 | execute: () => 'original execution',
22 | };
23 |
24 | const invalidExecute = 'invalid';
25 |
26 | // @ts-expect-error
27 | execute(builder, invalidExecute);
28 |
29 | expect(builder.execute()).toBe('original execution');
30 | });
31 |
32 | it('should handle undefined execute function gracefully', () => {
33 | const builder: ExecutableBuilder = {
34 | type: Builders.SelectMenu,
35 | execute: () => 'original execution',
36 | };
37 |
38 | // @ts-expect-error
39 | execute(builder, undefined);
40 |
41 | expect(builder.execute()).toBe('original execution');
42 | });
43 |
44 | it('should not modify builder if execute is not a function', () => {
45 | const builder: ExecutableBuilder = {
46 | type: Builders.ContextMenu,
47 | execute: () => 'original execution',
48 | };
49 |
50 | const invalidExecute = 123;
51 |
52 | // @ts-expect-error
53 | execute(builder, invalidExecute);
54 |
55 | expect(builder.execute()).toBe('original execution');
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/execute/execute.ts:
--------------------------------------------------------------------------------
1 | import type { Builder } from '../../types';
2 |
3 | export type ExecutableBuilder = Pick;
4 |
5 | /**
6 | * Set the function to execute when an executable builder is accepted.
7 | *
8 | * @param builder The builder to mutate
9 | * @param execute Callback function to execute when a builder is accepted
10 | *
11 | * @see https://sunar.js.org/docs/mutators/execute
12 | */
13 | export function execute(builder: TBuilder, execute: TBuilder['execute']): void {
14 | if (typeof execute !== 'function') return;
15 | builder.execute = execute;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/execute/index.ts:
--------------------------------------------------------------------------------
1 | export * from './execute';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config';
2 | export * from './execute';
3 | export * from './protect';
4 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/protect/index.ts:
--------------------------------------------------------------------------------
1 | export * from './protect';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/protect/protect.spec.ts:
--------------------------------------------------------------------------------
1 | import { Protector } from '../../builders';
2 | import { Builders } from '../../utils';
3 | import { type ProtectableBuilder, protect } from './protect';
4 |
5 | describe('protect()', () => {
6 | it('should add protectors to the builder', () => {
7 | const builder: ProtectableBuilder = {
8 | type: Builders.Slash,
9 | protectors: [],
10 | };
11 |
12 | const newProtectors: Protector[] = [
13 | new Protector({ commands: ['slash'] }),
14 | new Protector({ commands: ['contextMenu'] }),
15 | ];
16 |
17 | const result = protect(builder, newProtectors);
18 |
19 | expect(result).toHaveLength(2);
20 | expect(result).toEqual(newProtectors);
21 | expect(builder.protectors).toEqual(newProtectors);
22 | });
23 |
24 | it('should append protectors to existing protectors', () => {
25 | const existingProtector = new Protector({ commands: ['slash'] });
26 |
27 | const builder: ProtectableBuilder = {
28 | type: Builders.Button,
29 | protectors: [existingProtector],
30 | };
31 |
32 | const newProtectors: Protector[] = [
33 | new Protector({ components: ['button'] }),
34 | new Protector({ components: ['modal'] }),
35 | ];
36 |
37 | const result = protect(builder, newProtectors);
38 |
39 | expect(result).toHaveLength(3);
40 | expect(builder.protectors).toEqual([existingProtector, ...newProtectors]);
41 | });
42 |
43 | it('should handle undefined protectors gracefully', () => {
44 | const builder: ProtectableBuilder = {
45 | type: Builders.SelectMenu,
46 | // @ts-expect-error
47 | protectors: undefined,
48 | };
49 |
50 | const newProtectors: Protector[] = [new Protector({ components: ['selectMenu'] })];
51 |
52 | const result = protect(builder, newProtectors);
53 |
54 | expect(result).toBeUndefined();
55 | });
56 |
57 | it('should not modify builder if protectors is not an array', () => {
58 | const builder: ProtectableBuilder = {
59 | type: Builders.ContextMenu,
60 | // @ts-expect-error
61 | protectors: 'invalid',
62 | };
63 |
64 | const newProtectors: Protector[] = [new Protector({ commands: ['contextMenu'] })];
65 |
66 | const result = protect(builder, newProtectors);
67 |
68 | expect(result).toBe('invalid');
69 | expect(builder.protectors).toBe('invalid');
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/packages/sunar/src/mutators/protect/protect.ts:
--------------------------------------------------------------------------------
1 | import type { Protector } from '../../builders';
2 | import type { Builder } from '../../types';
3 |
4 | export type ProtectableBuilder = Pick;
5 |
6 | // FIXME: Improve types, "any" should not be used here
7 |
8 | /**
9 | * Add a middleware to a builder.
10 | *
11 | * @param builder The builder to mutate
12 | * @param protectors An array of protectors that will be added to the builder
13 | * @returns The new protectors array
14 | *
15 | * @see https://sunar.js.org/docs/mutators/protect
16 | * @see https://sunar.js.org/docs/guides/middlewares#create-the-protector-logic
17 | * @see https://sunar.js.org/docs/guides/middlewares#create-a-protected-command
18 | */
19 | export function protect(builder: TBuilder, protectors: Protector[]): Protector[] {
20 | if (!Array.isArray(builder.protectors)) return builder.protectors;
21 | builder.protectors.push(...(protectors as any));
22 | return builder.protectors;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/sunar/src/registry/dynamic/dynamic.ts:
--------------------------------------------------------------------------------
1 | import { type ApplicationCommand, type ApplicationCommandData, type ClientApplication, Collection } from 'discord.js';
2 |
3 | import { type SunarApplicationCommand, getSunarApplicationCommands } from '../../utils';
4 |
5 | export interface DynamicRegistryResult {
6 | globalCommands: Collection;
7 | guildCommands: Collection[];
8 | }
9 |
10 | /**
11 | * Register global and guild commands, by default all will be global, to specify that is a guild command add their IDs in the command configuration with the config mutator.
12 | *
13 | * @param application The client application where the commands will be registered.
14 | *
15 | * @returns An object with the registered global and guild commands
16 | *
17 | * @see https://sunar.js.org/docs/guides/registering-commands/dynamic
18 | */
19 | export async function registerCommands(application: ClientApplication): Promise {
20 | const commands = getSunarApplicationCommands();
21 |
22 | const isGuildCommand = (command: SunarApplicationCommand) =>
23 | command.config.guildIds && command.config.guildIds.length > 0;
24 |
25 | const globalCommands = commands.filter((c) => !isGuildCommand(c));
26 | const guildCommands = commands.filter(isGuildCommand);
27 |
28 | let globalCommandsResult: DynamicRegistryResult['globalCommands'] = new Collection();
29 |
30 | if (globalCommands.length > 0) {
31 | const result = await application.commands.set(globalCommands.map((c) => c.data));
32 | globalCommandsResult = result;
33 | }
34 |
35 | const guildCommandsResults: DynamicRegistryResult['guildCommands'] = [];
36 |
37 | if (guildCommands.length > 0) {
38 | const mappedGuilds: Record = {};
39 |
40 | for (const { config, data } of guildCommands) {
41 | if (!config.guildIds || config.guildIds.length <= 0) continue;
42 |
43 | for (const guildId of config.guildIds) {
44 | if (mappedGuilds[guildId]) mappedGuilds[guildId].push(data);
45 | else mappedGuilds[guildId] = [data];
46 | }
47 | }
48 |
49 | for (const entries of Object.entries(mappedGuilds)) {
50 | const [guildId, command] = entries;
51 |
52 | const result = await application.commands.set(command, guildId);
53 | guildCommandsResults.push(result);
54 | }
55 | }
56 |
57 | return {
58 | globalCommands: globalCommandsResult,
59 | guildCommands: guildCommandsResults,
60 | };
61 | }
62 |
--------------------------------------------------------------------------------
/packages/sunar/src/registry/dynamic/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dynamic';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/registry/global/global.ts:
--------------------------------------------------------------------------------
1 | import type { ApplicationCommand, ClientApplication, Collection } from 'discord.js';
2 |
3 | import { getApplicationCommands } from '../../utils';
4 |
5 | /**
6 | * Register all commands as global.
7 | *
8 | * @param application The client application where the commands will be registered.
9 | * @returns A registered application commands collection
10 | *
11 | * @see https://sunar.js.org/docs/guides/registering-commands/global
12 | */
13 | export function registerGlobalCommands(
14 | application: ClientApplication,
15 | ): Promise> {
16 | const applicationCommands = getApplicationCommands();
17 |
18 | return application.commands.set(applicationCommands.length > 0 ? applicationCommands : []);
19 | }
20 |
--------------------------------------------------------------------------------
/packages/sunar/src/registry/global/index.ts:
--------------------------------------------------------------------------------
1 | export * from './global';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/registry/guild/guild.ts:
--------------------------------------------------------------------------------
1 | import type { ApplicationCommand, ClientApplication, Collection } from 'discord.js';
2 |
3 | import { getApplicationCommands } from '../../utils';
4 |
5 | /**
6 | * Register all commands for specific guilds.
7 | *
8 | * @param application The client application where the commands will be registered.
9 | * @param guildIds The IDs of the guilds where the commands will be registered.
10 | *
11 | * @returns An array of registered application commands collection
12 | *
13 | * @see https://sunar.js.org/docs/guides/registering-commands/guilds
14 | */
15 | export async function registerGuildCommands(
16 | application: ClientApplication,
17 | guildIds: string[],
18 | ): Promise[]> {
19 | const applicationCommands = getApplicationCommands();
20 |
21 | if (applicationCommands.length <= 0) return [];
22 |
23 | const results: Collection[] = [];
24 |
25 | for (const guildId of guildIds) {
26 | const result = await application.commands.set(applicationCommands, guildId);
27 | results.push(result);
28 | }
29 |
30 | return results;
31 | }
32 |
--------------------------------------------------------------------------------
/packages/sunar/src/registry/guild/index.ts:
--------------------------------------------------------------------------------
1 | export * from './guild';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/registry/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dynamic';
2 | export * from './global';
3 | export * from './guild';
4 |
--------------------------------------------------------------------------------
/packages/sunar/src/stores/collections.ts:
--------------------------------------------------------------------------------
1 | import { Collection } from 'discord.js';
2 |
3 | import type { CooldownTimestamp } from '..';
4 | import type { Autocomplete, Button, ContextMenu, Group, Modal, SelectMenu, Signal, Slash } from '../builders';
5 | import type { Builders, CooldownScope } from '../utils';
6 |
7 | export const signals = new Collection();
8 |
9 | export const groups = new Collection();
10 |
11 | export const slashCommands = new Collection();
12 |
13 | export const contextMenuCommands = new Collection();
14 |
15 | export const buttons = new Collection();
16 |
17 | export const modals = new Collection();
18 |
19 | export const selectMenus = new Collection();
20 |
21 | export const autocompletes = new Collection();
22 |
23 | export const cooldownManager = new Collection<
24 | Builders,
25 | Collection>
26 | >();
27 |
--------------------------------------------------------------------------------
/packages/sunar/src/stores/context.ts:
--------------------------------------------------------------------------------
1 | import type { Client } from '../client';
2 |
3 | export interface Context {
4 | client: Client;
5 | config: unknown;
6 | }
7 |
8 | export const context = {
9 | config: {},
10 | } as Context;
11 |
--------------------------------------------------------------------------------
/packages/sunar/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | export * from './collections';
2 | export * from './context';
3 |
--------------------------------------------------------------------------------
/packages/sunar/src/symbols.ts:
--------------------------------------------------------------------------------
1 | export const PROTECTOR_NEXT_SYMBOL = Symbol();
2 | export const UNHANDLED_SYMBOL = Symbol();
3 |
--------------------------------------------------------------------------------
/packages/sunar/src/types/builder.ts:
--------------------------------------------------------------------------------
1 | import type { Awaitable } from 'discord.js';
2 |
3 | import type { Protector } from '../builders';
4 | import type { Builders, Commands, Components } from '../utils';
5 |
6 | export interface Builder {
7 | readonly type: Builders;
8 | protectors: Protector[];
9 | execute: (...args: any) => Awaitable;
10 | config: object;
11 | }
12 |
13 | export type CommandKey = (typeof Commands)[keyof typeof Commands];
14 |
15 | export type ComponentKey = (typeof Components)[keyof typeof Components];
16 |
--------------------------------------------------------------------------------
/packages/sunar/src/types/config.ts:
--------------------------------------------------------------------------------
1 | import type { CooldownResolvable } from '..';
2 |
3 | export interface CooldownProp {
4 | /**
5 | * Defines the cooldown period for the command.
6 | * Can be a simple number representing milliseconds or a more complex configuration object.
7 | *
8 | * @see https://sunar.js.org/docs/guides/implementing-cooldowns
9 | */
10 | cooldown?: CooldownResolvable;
11 | }
12 |
13 | /** Configuration for a command. */
14 | export interface CommandConfig extends CooldownProp {
15 | /**
16 | * Specifies the guild IDs where the command is registered.
17 | * If provided, the command will only be available in these guilds.
18 | *
19 | * @see https://sunar.js.org/docs/guides/registering-commands/dynamic#specific-guilds-ids-commands
20 | */
21 | guildIds?: string[];
22 | }
23 |
--------------------------------------------------------------------------------
/packages/sunar/src/types/cooldown.ts:
--------------------------------------------------------------------------------
1 | import type { CooldownScope } from '..';
2 |
3 | /** Represents a timestamp for a cooldown. */
4 | export interface CooldownTimestamp {
5 | /** The ID of the target (user, channel, guild, or global) for the cooldown. */
6 | targetId: string | symbol;
7 |
8 | /** The expiration time of the cooldown in milliseconds. */
9 | expiration: number;
10 |
11 | /** The timer associated with the cooldown. */
12 | timer: NodeJS.Timeout;
13 | }
14 |
15 | /**
16 | * A type that resolves to a cooldown configuration.
17 | * Can be either a number (milliseconds) or a more detailed configuration object.
18 | */
19 | export type CooldownResolvable = number | CooldownConfig;
20 |
21 | /** Configuration for cooldowns. */
22 | export type CooldownConfig = TScope extends CooldownScope.Global
23 | ? {
24 | /** The cooldown time in milliseconds. */
25 | time: number;
26 |
27 | /** Optional limit for the number of uses before the cooldown applies. */
28 | limit?: number;
29 |
30 | /** The scope of the cooldown, set to global. */
31 | scope: CooldownScope.Global;
32 | }
33 | : {
34 | /** The cooldown time in milliseconds. */
35 | time: number;
36 |
37 | /** Optional array of IDs to exclude from the cooldown. */
38 | exclude?: string[];
39 |
40 | /** Optional limit for the number of uses before the cooldown applies. */
41 | limit?: number;
42 |
43 | /** The scope of the cooldown, defaults to the provided scope. */
44 | scope?: TScope;
45 | };
46 |
47 | /** Context for the cooldown. */
48 | export interface CooldownContext {
49 | /** The remaining time of the cooldown in milliseconds. */
50 | remaining: number;
51 |
52 | /** The scope of the cooldown. */
53 | scope: CooldownScope;
54 |
55 | /** The limit for the number of uses before the cooldown applies. */
56 | limit: number;
57 | }
58 |
--------------------------------------------------------------------------------
/packages/sunar/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './builder';
2 | export * from './config';
3 | export * from './cooldown';
4 | export * from './signals';
5 | export * from './utils';
6 |
--------------------------------------------------------------------------------
/packages/sunar/src/types/signals.ts:
--------------------------------------------------------------------------------
1 | import type { RepliableInteraction } from 'discord.js';
2 |
3 | import type { Signals } from '../utils';
4 | import type { CooldownContext } from './cooldown';
5 |
6 | export interface SunarSignals {
7 | [Signals.Cooldown]: [interaction: RepliableInteraction, context: CooldownContext];
8 | }
9 |
--------------------------------------------------------------------------------
/packages/sunar/src/types/utils.ts:
--------------------------------------------------------------------------------
1 | export type Prettify = { [TKey in keyof TObject]: TObject[TKey] } & {};
2 |
3 | export type NextFunction = () => symbol;
4 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import { Events } from 'discord.js';
2 |
3 | export const Signals = {
4 | ...Events,
5 | Cooldown: 'cooldown',
6 | } as const;
7 |
8 | export const Commands = {
9 | Slash: 'slash',
10 | ContextMenu: 'contextMenu',
11 | Autocomplete: 'autocomplete',
12 | } as const;
13 |
14 | export const Components = {
15 | Button: 'button',
16 | Modal: 'modal',
17 | SelectMenu: 'selectMenu',
18 | } as const;
19 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/enums.ts:
--------------------------------------------------------------------------------
1 | export enum Builders {
2 | Slash = 0,
3 | ContextMenu = 1,
4 | Protector = 2,
5 | Signal = 3,
6 | Button = 4,
7 | Modal = 5,
8 | SelectMenu = 6,
9 | Autocomplete = 7,
10 | Group = 8
11 | }
12 |
13 | export enum CooldownScope {
14 | User = 0,
15 | Channel = 1,
16 | Guild = 2,
17 | Global = 3,
18 | }
19 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/getApplicationCommands/getApplicationCommands.ts:
--------------------------------------------------------------------------------
1 | import type { ApplicationCommandData } from 'discord.js';
2 |
3 | import { contextMenuCommands, slashCommands } from '../../stores';
4 |
5 | export function getApplicationCommands(): ApplicationCommandData[] {
6 | if (contextMenuCommands.size <= 0 && slashCommands.size <= 0) return [];
7 | const commands = [...contextMenuCommands.values(), ...slashCommands.values()];
8 | const applicationCommands = commands.map(({ data }) => data);
9 | return applicationCommands;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/getApplicationCommands/index.ts:
--------------------------------------------------------------------------------
1 | export * from './getApplicationCommands';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/getGroupStoreKey/getGroupStoreKey.ts:
--------------------------------------------------------------------------------
1 | import type { Group } from '../../builders';
2 | import { isGroupBuilder } from '../isGroupBuilder';
3 |
4 | export function getGroupStoreKey(root: string | Group, parent?: string | null, sub?: string | null) {
5 | if (isGroupBuilder(root)) {
6 | return `${root.root}_${root.parent && root.sub ? `${root.parent}_${root.sub}` : root.parent}`;
7 | }
8 | return `${root}_${parent && sub ? `${parent}_${sub}` : sub}`;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/getGroupStoreKey/index.ts:
--------------------------------------------------------------------------------
1 | export * from './getGroupStoreKey';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/getSunarApplicationCommands/getSunarApplicationCommands.ts:
--------------------------------------------------------------------------------
1 | import type { ApplicationCommandData } from 'discord.js';
2 |
3 | import type { CommandConfig } from '../..';
4 | import { contextMenuCommands, slashCommands } from '../../stores';
5 |
6 | export interface SunarApplicationCommand {
7 | data: ApplicationCommandData;
8 | config: CommandConfig;
9 | }
10 |
11 | export function getSunarApplicationCommands(): SunarApplicationCommand[] {
12 | if (contextMenuCommands.size <= 0 && slashCommands.size <= 0) return [];
13 | const commands = [...contextMenuCommands.values(), ...slashCommands.values()];
14 | const applicationCommands = commands.map(({ data, config }) => ({ data, config }));
15 | return applicationCommands;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/getSunarApplicationCommands/index.ts:
--------------------------------------------------------------------------------
1 | export * from './getSunarApplicationCommands';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './enums';
3 | export * from './getApplicationCommands';
4 | export * from './getSunarApplicationCommands';
5 | export * from './isAutocompleteBuilder';
6 | export * from './isBuilder';
7 | export * from './isButtonBuilder';
8 | export * from './isContextMenuBuilder';
9 | export * from './isModalBuilder';
10 | export * from './isObject';
11 | export * from './isRegex';
12 | export * from './isSelectMenuBuilder';
13 | export * from './isSignalBuilder';
14 | export * from './isSlashBuilder';
15 | export * from './resolveCooldown';
16 | export * from './isGroupBuilder';
17 | export * from './getGroupStoreKey';
18 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isAutocompleteBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isAutocompleteBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isAutocompleteBuilder/isAutocompleteBuilder.spec.ts:
--------------------------------------------------------------------------------
1 | import { Autocomplete } from '../../builders';
2 | import { isAutocompleteBuilder } from './isAutocompleteBuilder';
3 |
4 | describe('isAutocompleteBuilder()', () => {
5 | it('should return true for a valid Autocomplete builder', () => {
6 | const autocompleteBuilder = new Autocomplete({ name: 'testAutocomplete' });
7 | expect(isAutocompleteBuilder(autocompleteBuilder)).toBe(true);
8 | });
9 |
10 | it('should return false for a builder of different type', () => {
11 | class MockBuilder {}
12 | const mockBuilder = new MockBuilder();
13 | expect(isAutocompleteBuilder(mockBuilder)).toBe(false);
14 | });
15 |
16 | it('should return false for an object that is not a builder', () => {
17 | const notABuilder = {
18 | title: 'Not a builder',
19 | };
20 | expect(isAutocompleteBuilder(notABuilder)).toBe(false);
21 | });
22 |
23 | it('should return false for null', () => {
24 | expect(isAutocompleteBuilder(null)).toBe(false);
25 | });
26 |
27 | it('should return false for undefined', () => {
28 | expect(isAutocompleteBuilder(undefined)).toBe(false);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isAutocompleteBuilder/isAutocompleteBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { Autocomplete } from '../../builders';
3 |
4 | export function isAutocompleteBuilder(builder: any): builder is Autocomplete {
5 | if (!isBuilder(builder)) return false;
6 | return builder instanceof Autocomplete && builder.type === Builders.Autocomplete;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isBuilder/isBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isObject } from '..';
2 | import type { Builder } from '../../types';
3 |
4 | export function isBuilder(builder: any): builder is Builder {
5 | if (!isObject(builder)) return false;
6 | const hasType = 'type' in builder;
7 | return hasType && typeof builder.type === 'number' && Boolean(Builders[builder.type]);
8 | }
9 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isButtonBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isButtonBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isButtonBuilder/isButtonBuilder.spec.ts:
--------------------------------------------------------------------------------
1 | import { Button, ContextMenu } from '../../builders';
2 | import { isButtonBuilder } from './isButtonBuilder';
3 |
4 | describe('isButtonBuilder()', () => {
5 | it('should return true for a valid Button builder', () => {
6 | const buttonBuilder = new Button({ id: 'testButton' });
7 | expect(isButtonBuilder(buttonBuilder)).toBe(true);
8 | });
9 |
10 | it('should return false for a builder of different type', () => {
11 | const contextMenuBuilder = new ContextMenu({ name: 'testContextMenu', type: 2 });
12 | expect(isButtonBuilder(contextMenuBuilder)).toBe(false);
13 | });
14 |
15 | it('should return false for an object that is not a builder', () => {
16 | const notABuilder = {
17 | title: 'Not a builder',
18 | };
19 | expect(isButtonBuilder(notABuilder)).toBe(false);
20 | });
21 |
22 | it('should return false for null', () => {
23 | expect(isButtonBuilder(null)).toBe(false);
24 | });
25 |
26 | it('should return false for undefined', () => {
27 | expect(isButtonBuilder(undefined)).toBe(false);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isButtonBuilder/isButtonBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { Button } from '../../builders';
3 |
4 | export function isButtonBuilder(builder: any): builder is Button {
5 | if (!isBuilder(builder)) return false;
6 | return builder instanceof Button && builder.type === Builders.Button;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isContextMenuBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isContextMenuBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isContextMenuBuilder/isContextMenuBuilder.spec.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationCommandType } from 'discord.js';
2 | import { Builders } from '..';
3 | import { Button, ContextMenu } from '../../builders';
4 | import { isContextMenuBuilder } from './isContextMenuBuilder';
5 |
6 | describe('isContextMenuBuilder()', () => {
7 | it('should return true for a valid ContextMenu builder', () => {
8 | const contextMenuBuilder = new ContextMenu({ name: 'testContextMenu', type: 2 });
9 | expect(isContextMenuBuilder(contextMenuBuilder)).toBe(true);
10 | });
11 |
12 | it('should return false for a builder of different type', () => {
13 | const buttonBuilder = new Button({ id: 'testButton' });
14 | expect(isContextMenuBuilder(buttonBuilder)).toBe(false);
15 | });
16 |
17 | it('should return false for an object that is not a builder', () => {
18 | const notABuilder = {
19 | title: 'Not a builder',
20 | };
21 | expect(isContextMenuBuilder(notABuilder)).toBe(false);
22 | });
23 |
24 | it('should return false for null', () => {
25 | expect(isContextMenuBuilder(null)).toBe(false);
26 | });
27 |
28 | it('should return false for undefined', () => {
29 | expect(isContextMenuBuilder(undefined)).toBe(false);
30 | });
31 |
32 | it('should return false for a builder without data property', () => {
33 | const invalidBuilder = {
34 | type: Builders.ContextMenu,
35 | };
36 | expect(isContextMenuBuilder(invalidBuilder)).toBe(false);
37 | });
38 |
39 | it('should return false for a builder with data property but without name', () => {
40 | // @ts-expect-error
41 | const invalidBuilder = new ContextMenu({ type: ApplicationCommandType.User });
42 | expect(isContextMenuBuilder(invalidBuilder)).toBe(false);
43 | });
44 |
45 | it('should return false for a builder with data property but without type', () => {
46 | // @ts-expect-error
47 | const invalidBuilder = new ContextMenu({ name: 'invalidBuilder' });
48 | expect(isContextMenuBuilder(invalidBuilder)).toBe(false);
49 | });
50 |
51 | it('should return false for a builder with invalid data property', () => {
52 | // @ts-expect-error
53 | const invalidBuilder = new ContextMenu('invalidData');
54 | expect(isContextMenuBuilder(invalidBuilder)).toBe(false);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isContextMenuBuilder/isContextMenuBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { ContextMenu } from '../../builders';
3 |
4 | export function isContextMenuBuilder(builder: any): builder is ContextMenu {
5 | if (!isBuilder(builder)) return false;
6 |
7 | const hasData = 'data' in builder;
8 | if (!hasData || typeof builder.data !== 'object' || !builder.data) return false;
9 |
10 | const hasName = 'name' in builder.data;
11 | const hasType = 'type' in builder.data;
12 |
13 | return builder instanceof ContextMenu && builder.type === Builders.ContextMenu && hasName && hasType;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isGroupBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isGroupBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isGroupBuilder/isGroupBuilder.spec.ts:
--------------------------------------------------------------------------------
1 | import { Builders } from '..';
2 | import { Button, Group } from '../../builders';
3 | import { isGroupBuilder } from './isGroupBuilder';
4 |
5 | describe('isGroupBuilder()', () => {
6 | it('should return true for a valid Group builder', () => {
7 | const groupBuilder = new Group('example', 'parent');
8 | expect(isGroupBuilder(groupBuilder)).toBe(true);
9 | });
10 |
11 | it('should return false for a builder of different type', () => {
12 | const buttonBuilder = new Button({ id: 'testButton' });
13 | expect(isGroupBuilder(buttonBuilder)).toBe(false);
14 | });
15 |
16 | it('should return false for an object that is not a builder', () => {
17 | const notABuilder = {
18 | title: 'Not a builder',
19 | };
20 | expect(isGroupBuilder(notABuilder)).toBe(false);
21 | });
22 |
23 | it('should return false for null', () => {
24 | expect(isGroupBuilder(null)).toBe(false);
25 | });
26 |
27 | it('should return false for undefined', () => {
28 | expect(isGroupBuilder(undefined)).toBe(false);
29 | });
30 |
31 | it('should return false for a builder without data property', () => {
32 | const invalidBuilder = {
33 | type: Builders.Group,
34 | };
35 | expect(isGroupBuilder(invalidBuilder)).toBe(false);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isGroupBuilder/isGroupBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { Group } from '../../builders';
3 |
4 | export function isGroupBuilder(builder: any): builder is Group {
5 | if (!isBuilder(builder)) return false;
6 |
7 | const hasRoot = 'root' in builder;
8 | const hasParent = 'parent' in builder;
9 |
10 | return builder instanceof Group && builder.type === Builders.Group && hasRoot && hasParent;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isModalBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isModalBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isModalBuilder/isModalBuilder.spec.ts:
--------------------------------------------------------------------------------
1 | import { Builders } from '..';
2 | import { Button, Modal } from '../../builders';
3 | import { isModalBuilder } from './isModalBuilder';
4 |
5 | describe('isModalBuilder()', () => {
6 | it('should return true for a valid Modal builder', () => {
7 | const modalBuilder = new Modal({ id: 'testModal' });
8 | expect(isModalBuilder(modalBuilder)).toBe(true);
9 | });
10 |
11 | it('should return false for a button builder', () => {
12 | const buttonBuilder = new Button({ id: 'testButton' });
13 | expect(isModalBuilder(buttonBuilder)).toBe(false);
14 | });
15 |
16 | it('should return false for an object that is not a builder', () => {
17 | const notABuilder = {
18 | title: 'Not a builder',
19 | };
20 | expect(isModalBuilder(notABuilder)).toBe(false);
21 | });
22 |
23 | it('should return false for null', () => {
24 | expect(isModalBuilder(null)).toBe(false);
25 | });
26 |
27 | it('should return false for undefined', () => {
28 | expect(isModalBuilder(undefined)).toBe(false);
29 | });
30 |
31 | it('should return false for a builder of different type', () => {
32 | class DifferentBuilder {
33 | constructor(public type: Builders) {}
34 | }
35 | const differentBuilder = new DifferentBuilder(Builders.Slash);
36 | expect(isModalBuilder(differentBuilder)).toBe(false);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isModalBuilder/isModalBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { Modal } from '../../builders';
3 |
4 | export function isModalBuilder(builder: any): builder is Modal {
5 | if (!isBuilder(builder)) return false;
6 | return builder instanceof Modal && builder.type === Builders.Modal;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isObject/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isObject';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isObject/isObject.spec.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from './isObject';
2 |
3 | describe('isObject()', () => {
4 | it('should return true for plain objects', () => {
5 | expect(isObject({})).toBe(true);
6 | expect(isObject({ key: 'value' })).toBe(true);
7 | });
8 |
9 | it('should return false for null', () => {
10 | expect(isObject(null)).toBe(false);
11 | });
12 |
13 | it('should return false for undefined', () => {
14 | expect(isObject(undefined)).toBe(false);
15 | });
16 |
17 | it('should return false for primitive types', () => {
18 | expect(isObject(42)).toBe(false);
19 | expect(isObject('string')).toBe(false);
20 | expect(isObject(true)).toBe(false);
21 | expect(isObject(Symbol('symbol'))).toBe(false);
22 | expect(isObject(BigInt(123))).toBe(false);
23 | });
24 |
25 | it('should return true for arrays', () => {
26 | expect(isObject([])).toBe(true);
27 | expect(isObject([1, 2, 3])).toBe(true);
28 | });
29 |
30 | it('should return false for functions', () => {
31 | expect(isObject(() => {})).toBe(false);
32 | // biome-ignore lint/complexity/useArrowFunction: testing purposes
33 | expect(isObject(function () {})).toBe(false);
34 | });
35 |
36 | it('should return true for instances of classes', () => {
37 | class MyClass {}
38 | expect(isObject(new MyClass())).toBe(true);
39 | });
40 |
41 | it('should return true for other non-plain objects', () => {
42 | expect(isObject(new Date())).toBe(true);
43 | // biome-ignore lint/performance/useTopLevelRegex: testing purposes
44 | expect(isObject(/regex/)).toBe(true);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isObject/isObject.ts:
--------------------------------------------------------------------------------
1 | export function isObject(object?: any): object is object {
2 | return typeof object === 'object' && object != null;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isRegex/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isRegex';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isRegex/isRegex.spec.ts:
--------------------------------------------------------------------------------
1 | import { isRegex } from './isRegex';
2 |
3 | describe('isRegex()', () => {
4 | it('should return true for a valid RegExp', () => {
5 | // biome-ignore lint/performance/useTopLevelRegex: testing purposes
6 | const regex = /[a-z]+/;
7 | expect(isRegex(regex)).toBe(true);
8 | });
9 |
10 | it('should return false for a string', () => {
11 | const notRegex = 'not a regex';
12 | expect(isRegex(notRegex)).toBe(false);
13 | });
14 |
15 | it('should return false for a number', () => {
16 | const notRegex = 42;
17 | expect(isRegex(notRegex)).toBe(false);
18 | });
19 |
20 | it('should return false for null', () => {
21 | // biome-ignore lint/suspicious/noEvolvingTypes: testing purposes
22 | const notRegex = null;
23 | expect(isRegex(notRegex)).toBe(false);
24 | });
25 |
26 | it('should return false for undefined', () => {
27 | const notRegex = undefined;
28 | expect(isRegex(notRegex)).toBe(false);
29 | });
30 |
31 | it('should return false for an object', () => {
32 | const notRegex = { pattern: '[a-z]+' };
33 | expect(isRegex(notRegex)).toBe(false);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isRegex/isRegex.ts:
--------------------------------------------------------------------------------
1 | export function isRegex(regex: any): regex is RegExp {
2 | return regex instanceof RegExp;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSelectMenuBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isSelectMenuBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSelectMenuBuilder/isSelectMenuBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { SelectMenu } from '../../builders';
3 |
4 | export function isSelectMenuBuilder(builder: any): builder is SelectMenu {
5 | if (!isBuilder(builder)) return false;
6 | return builder instanceof SelectMenu && builder.type === Builders.SelectMenu;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSignalBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isSignalBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSignalBuilder/isSignalBuilder.spec.ts:
--------------------------------------------------------------------------------
1 | import { Builders } from '..';
2 | import { Signal } from '../../builders';
3 | import { isSignalBuilder } from './isSignalBuilder';
4 |
5 | describe('isSignalBuilder()', () => {
6 | it('should return true for a valid Signal builder', () => {
7 | const signalBuilder = new Signal('messageCreate');
8 | expect(isSignalBuilder(signalBuilder)).toBe(true);
9 | });
10 |
11 | it('should return false for a builder of different type', () => {
12 | class DifferentBuilder {
13 | constructor(public type: Builders) {}
14 | }
15 | const differentBuilder = new DifferentBuilder(Builders.Button);
16 | expect(isSignalBuilder(differentBuilder)).toBe(false);
17 | });
18 |
19 | it('should return false for an object that is not a builder', () => {
20 | const notABuilder = {
21 | title: 'Not a builder',
22 | };
23 | expect(isSignalBuilder(notABuilder)).toBe(false);
24 | });
25 |
26 | it('should return false for null', () => {
27 | expect(isSignalBuilder(null)).toBe(false);
28 | });
29 |
30 | it('should return false for undefined', () => {
31 | expect(isSignalBuilder(undefined)).toBe(false);
32 | });
33 |
34 | it('should return false for a builder without required properties', () => {
35 | const invalidBuilder = {
36 | type: Builders.Signal,
37 | };
38 | expect(isSignalBuilder(invalidBuilder)).toBe(false);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSignalBuilder/isSignalBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { Signal } from '../../builders';
3 |
4 | export function isSignalBuilder(builder: any): builder is Signal {
5 | if (!isBuilder(builder)) return false;
6 | const hasName = 'name' in builder;
7 | return builder instanceof Signal && builder.type === Builders.Signal && hasName;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSlashBuilder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './isSlashBuilder';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSlashBuilder/isSlashBuilder.spec.ts:
--------------------------------------------------------------------------------
1 | import { Builders } from '..';
2 | import { Button, Slash } from '../../builders';
3 | import { isSlashBuilder } from './isSlashBuilder';
4 |
5 | describe('isSlashBuilder()', () => {
6 | it('should return true for a valid Slash builder', () => {
7 | const slashBuilder = new Slash({ name: 'testCommand', description: 'Test command' });
8 | expect(isSlashBuilder(slashBuilder)).toBe(true);
9 | });
10 |
11 | it('should return false for a builder of different type', () => {
12 | const buttonBuilder = new Button({ id: 'testButton' });
13 | expect(isSlashBuilder(buttonBuilder)).toBe(false);
14 | });
15 |
16 | it('should return false for an object that is not a builder', () => {
17 | const notABuilder = {
18 | title: 'Not a builder',
19 | };
20 | expect(isSlashBuilder(notABuilder)).toBe(false);
21 | });
22 |
23 | it('should return false for null', () => {
24 | expect(isSlashBuilder(null)).toBe(false);
25 | });
26 |
27 | it('should return false for undefined', () => {
28 | expect(isSlashBuilder(undefined)).toBe(false);
29 | });
30 |
31 | it('should return false for a builder without data property', () => {
32 | const invalidBuilder = {
33 | type: Builders.Slash,
34 | };
35 | expect(isSlashBuilder(invalidBuilder)).toBe(false);
36 | });
37 |
38 | it('should return false for a builder with data property but without name', () => {
39 | // @ts-expect-error
40 | const invalidBuilder = new Slash({ description: 'Test command' });
41 | expect(isSlashBuilder(invalidBuilder)).toBe(false);
42 | });
43 |
44 | it('should return false for a builder with data property but without description', () => {
45 | // @ts-expect-error
46 | const invalidBuilder = new Slash({ name: 'testCommand' });
47 | expect(isSlashBuilder(invalidBuilder)).toBe(false);
48 | });
49 |
50 | it('should return false for a builder with invalid data property', () => {
51 | // @ts-expect-error
52 | const invalidBuilder = new Slash('invalidData');
53 | expect(isSlashBuilder(invalidBuilder)).toBe(false);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/isSlashBuilder/isSlashBuilder.ts:
--------------------------------------------------------------------------------
1 | import { Builders, isBuilder } from '..';
2 | import { Slash } from '../../builders';
3 |
4 | export function isSlashBuilder(builder: any): builder is Slash {
5 | if (!isBuilder(builder)) return false;
6 |
7 | const hasData = 'data' in builder;
8 | if (!hasData || typeof builder.data !== 'object' || !builder.data) return false;
9 |
10 | const hasName = 'name' in builder.data;
11 | const hasDescription = 'description' in builder.data;
12 |
13 | return builder instanceof Slash && builder.type === Builders.Slash && hasName && hasDescription;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/resolveCooldown/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resolveCooldown';
2 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/resolveCooldown/resolveCooldown.spec.ts:
--------------------------------------------------------------------------------
1 | import { CooldownScope } from '../enums';
2 | import { resolveCooldown } from './resolveCooldown';
3 |
4 | describe('resolveCooldown()', () => {
5 | it('should resolve a simple number cooldown', () => {
6 | const cooldown = 5000;
7 | const resolved = resolveCooldown(cooldown);
8 |
9 | expect(resolved.time).toBe(5000);
10 | expect(resolved.limit).toBe(1);
11 | expect(resolved.scope).toBe(CooldownScope.User);
12 | expect(resolved.exclude).toEqual([]);
13 | });
14 |
15 | it('should resolve a global cooldown configuration', () => {
16 | const cooldown = {
17 | time: 10000,
18 | scope: CooldownScope.Global,
19 | };
20 | const resolved = resolveCooldown(cooldown);
21 |
22 | expect(resolved.time).toBe(10000);
23 | expect(resolved.limit).toBe(1);
24 | expect(resolved.scope).toBe(CooldownScope.Global);
25 | expect(resolved.exclude).toEqual([]);
26 | });
27 |
28 | it('should resolve a scoped cooldown configuration', () => {
29 | const cooldown = {
30 | time: 20000,
31 | limit: 3,
32 | scope: CooldownScope.Channel,
33 | exclude: ['123456789'],
34 | };
35 | const resolved = resolveCooldown(cooldown);
36 |
37 | expect(resolved.time).toBe(20000);
38 | expect(resolved.limit).toBe(3);
39 | expect(resolved.scope).toBe(CooldownScope.Channel);
40 | expect(resolved.exclude).toEqual(['123456789']);
41 | });
42 |
43 | it('should resolve a scoped cooldown configuration with default scope', () => {
44 | const cooldown = {
45 | time: 30000,
46 | limit: 2,
47 | exclude: ['987654321'],
48 | };
49 | const resolved = resolveCooldown(cooldown);
50 |
51 | expect(resolved.time).toBe(30000);
52 | expect(resolved.limit).toBe(2);
53 | expect(resolved.scope).toBe(CooldownScope.User);
54 | expect(resolved.exclude).toEqual(['987654321']);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/packages/sunar/src/utils/resolveCooldown/resolveCooldown.ts:
--------------------------------------------------------------------------------
1 | import { CooldownScope } from '../..';
2 | import type { CooldownConfig, CooldownResolvable } from '../../types';
3 |
4 | export function resolveCooldown(cooldown: CooldownResolvable): Required & { exclude: string[] } {
5 | const isNumber = typeof cooldown === 'number';
6 | const isGlobalScope = !isNumber && cooldown.scope === CooldownScope.Global;
7 |
8 | let time: number;
9 |
10 | let scope = CooldownScope.User;
11 | let limit = 1;
12 |
13 | let exclude: string[] = [];
14 |
15 | if (isNumber) {
16 | time = cooldown;
17 | } else {
18 | if (cooldown.scope) scope = cooldown.scope;
19 | if (cooldown.limit) limit = cooldown.limit;
20 | time = cooldown.time;
21 |
22 | if (!isGlobalScope) {
23 | exclude = cooldown.exclude ?? [];
24 | }
25 | }
26 |
27 | return { time, limit, scope, exclude };
28 | }
29 |
--------------------------------------------------------------------------------
/packages/sunar/src/vitest/commands/_ignored.ts:
--------------------------------------------------------------------------------
1 | import { Slash } from '../../builders'
2 | import { execute } from '../../mutators'
3 |
4 | const slash = new Slash({ name: 'ignored', description: 'Testing ignore property of load function' })
5 |
6 | execute(slash, (interaction) => {
7 | interaction.reply({ content: 'Hello World!' })
8 | })
9 |
10 | export { slash }
--------------------------------------------------------------------------------
/packages/sunar/src/vitest/commands/ping.ts:
--------------------------------------------------------------------------------
1 | import { Slash } from '../../builders'
2 | import { execute } from '../../mutators'
3 |
4 | const slash = new Slash({ name: 'ping', description: 'Pong' })
5 |
6 | execute(slash, (interaction) => {
7 | interaction.reply({ content: 'Pong!' })
8 | })
9 |
10 | export { slash }
--------------------------------------------------------------------------------
/packages/sunar/src/vitest/signals/ready.ts:
--------------------------------------------------------------------------------
1 | import { Signal } from '../../builders';
2 | import { execute } from '../../mutators';
3 | import { Signals } from '../../utils';
4 |
5 | const signal = new Signal(Signals.ClientReady);
6 |
7 | execute(signal, (client) => {
8 | // biome-ignore lint/suspicious/noConsoleLog: testing purposes
9 | // biome-ignore lint/suspicious/noConsole: testing purposes
10 | console.log(`${client.user.tag} ready!`);
11 | });
12 |
13 | export { signal };
14 |
--------------------------------------------------------------------------------
/packages/sunar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@total-typescript/tsconfig/bundler/no-dom/library-monorepo",
3 | "compilerOptions": {
4 | "module": "ES2022",
5 | "moduleResolution": "Bundler",
6 | "types": ["vitest/globals"]
7 | },
8 | "exclude": ["node_modules", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/sunar/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions';
4 | import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
5 |
6 | export default defineConfig({
7 | entry: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/vitest/**/*.ts'],
8 | format: ['cjs', 'esm'],
9 | clean: true,
10 | dts: true,
11 | treeshake: true,
12 | target: 'es2022',
13 | bundle: true,
14 | sourcemap: true,
15 | keepNames: true,
16 | skipNodeModulesBundle: true,
17 | esbuildPlugins: [
18 | esbuildPluginVersionInjector(),
19 | esbuildPluginFilePathExtensions({ esmExtension: 'js', cjsExtension: 'cjs' }),
20 | ],
21 | ignoreWatch: ['**/node_modules/**', '**/.git/**'],
22 | });
23 |
--------------------------------------------------------------------------------
/packages/sunar/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | globals: true,
6 | environment: 'node',
7 | include: ['**/*.spec.ts'],
8 | coverage: {
9 | provider: 'v8',
10 | reporter: ['text'],
11 | },
12 | pool: 'forks',
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'apps/*'
3 | - 'packages/*'
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "remoteCache": {
4 | "enabled": true
5 | },
6 | "ui": "tui",
7 | "tasks": {
8 | "build": {
9 | "dependsOn": ["^build"],
10 | "outputs": [".next/**", "!.next/cache/**", "dist/**"]
11 | },
12 | "lint": {
13 | "dependsOn": ["^build"],
14 | "inputs": ["../../.prettierrc", "src/**", "package.json"],
15 | "outputs": []
16 | },
17 | "format": {
18 | "dependsOn": ["^format"]
19 | },
20 | "test": {
21 | "dependsOn": ["^test"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------