How do you register paths for pieces? Also have an additional question when this one is solved.

I have a file structure as outlined below, I am wondering how I can make sapphire recognize this path and register the respective stores for that path. I am running into the issue of even when calling <Store>.registerPath and pointing to my pieces folder it isn't loading the commands I have.
No description
45 Replies
Aaron
Aaron5mo ago
import { SapphireClient } from '@sapphire/framework';
import { ClientOptions } from 'discord.js';

interface Options {}

export class MaidClient extends SapphireClient {
constructor(options: ClientOptions) {
super(options);

this.stores.registerPath('src/pieces');
}
}
import { SapphireClient } from '@sapphire/framework';
import { ClientOptions } from 'discord.js';

interface Options {}

export class MaidClient extends SapphireClient {
constructor(options: ClientOptions) {
super(options);

this.stores.registerPath('src/pieces');
}
}
I extend the SapphireClient (as I will be adding my own stuff later) and call registerPath in the constructor so it does so when the client is inited.
kyra
kyra5mo ago
It's best if you use absolute paths instead of relative ones, so if you want to use a path from CWD, path.resolve('src/pieces') will work just fine. However, you can debug the piece loader by assigning Store.logger (static property), which will be called on every loader operation, or define logger: { level: LogLevel.Trace } in your client options for the same behaviour (source). This will write a lot of lines showing what exactly is the loader loading and where it's crawling in the file system. If you're getting other errors, please let me know which error, runtime (Node.js, Deno, Bun), runtime version, Sapphire version, and if possible, a link to the Git repository to help investigate.
Aaron
Aaron5mo ago
Uhm, wow that is a lot of mess it seems to be loading from src/pieces and from my root folder
kyra
kyra5mo ago
Sapphire does some mess so you don't have to! <:iara_wink_peace:881533315126018128> Question, did you set loadDefaultErrorListeners: false? If you don't have an error listener, the loader may be erroring but with no listener listening to the errors, nothing is printed
Aaron
Aaron5mo ago
no I didn't I have a pretty basic implementation right now.
import '@sapphire/plugin-logger/register';

import { MaidClient } from './core';
import { IntentsBitField } from 'discord.js';
import { LogLevel } from '@sapphire/framework';

const Intents = IntentsBitField.Flags;

const client = new MaidClient({
intents: [Intents.Guilds],
logger: { level: LogLevel.Trace },
});

client.on('ready', (client) => {
client.logger.info(`${client.user.username} has logged in!`);
});

client.login('nope').catch((err) => {
console.warn(err);
});
import '@sapphire/plugin-logger/register';

import { MaidClient } from './core';
import { IntentsBitField } from 'discord.js';
import { LogLevel } from '@sapphire/framework';

const Intents = IntentsBitField.Flags;

const client = new MaidClient({
intents: [Intents.Guilds],
logger: { level: LogLevel.Trace },
});

client.on('ready', (client) => {
client.logger.info(`${client.user.username} has logged in!`);
});

client.login('nope').catch((err) => {
console.warn(err);
});
It appears to be loading everything in the place I specify but then trying to load it somewhere else overriding the previous store values
kyra
kyra5mo ago
You don't want to load anything at core, right? Only at pieces
Aaron
Aaron5mo ago
yes right now it is loading at C:\Dev\maid and C:\Dev\maid\src\pieces
kyra
kyra5mo ago
In that case you can use the baseUserDirectory Client option to override the default path, baseUserDirectory: path.resolve('src/pieces') should suffice Also, we recommend using listeners instead of client.on, it'll help organise your code
Aaron
Aaron5mo ago
I was going to add listeners but it wasn't registering the commands or listeners so I used client.on temporarily to see if calling registerPath in ready would mmLol see It's not loading my commands
Trace: 2024-01-24 00:30:31 - TRACE - [STORE => commands] [LOAD] Skipped piece 'C:\Dev\maid\src\pieces\commands\moderation\ModGroup.ts' as 'LoaderStrategy#filter' returned 'null'.
Trace: 2024-01-24 00:30:31 - TRACE - [STORE => commands] [LOAD] Skipped piece 'C:\Dev\maid\src\pieces\commands\moderation\ModGroup.ts' as 'LoaderStrategy#filter' returned 'null'.
kyra
kyra5mo ago
I'll need code then, what's in those commands?
Aaron
Aaron5mo ago
import { Subcommand } from '@sapphire/plugin-subcommands';

export class ModGroup extends Subcommand {
constructor(context: Subcommand.LoaderContext, options: Subcommand.Options) {
super(context, {
...options,
name: 'mod',
description: 'Moderation Commands',
subcommands: [
{
name: 'warn',
chatInputRun: 'chatInputWarn',
},
],
});
}

public override registerApplicationCommands(registry: Subcommand.Registry) {
registry.registerChatInputCommand((builder) => {
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((command) =>
command
.setName('warn')
.setDescription('Warn a given user')
.addUserOption((option) => option.setName('user').setDescription('user to give a warning to').setRequired(true)),
);
});
}
}
import { Subcommand } from '@sapphire/plugin-subcommands';

export class ModGroup extends Subcommand {
constructor(context: Subcommand.LoaderContext, options: Subcommand.Options) {
super(context, {
...options,
name: 'mod',
description: 'Moderation Commands',
subcommands: [
{
name: 'warn',
chatInputRun: 'chatInputWarn',
},
],
});
}

public override registerApplicationCommands(registry: Subcommand.Registry) {
registry.registerChatInputCommand((builder) => {
builder
.setName(this.name)
.setDescription(this.description)
.addSubcommand((command) =>
command
.setName('warn')
.setDescription('Warn a given user')
.addUserOption((option) => option.setName('user').setDescription('user to give a warning to').setRequired(true)),
);
});
}
}
kyra
kyra5mo ago
Oh... you're trying to run TypeScript code directly
Aaron
Aaron5mo ago
nope, I am using tsc first then running the js code
kyra
kyra5mo ago
Then you should be running the compiled code, which should ideally be in a folder named dist
Aaron
Aaron5mo ago
it indeed is I am running the resulting index.js file which is the project's main file after compiling
Favna
Favna5mo ago
Have a look at https://sapphirejs.dev/docs/Guide/additional-information/implementing-a-discordpy-like-cog-system. Although with that you will have to name events to listeners or register the custom name still.
Favna
Favna5mo ago
And there's also https://github.com/KBot-discord/plugins/tree/main/packages/modules which basically implements that cog system from the guide page.
GitHub
plugins/packages/modules at main · KBot-discord/plugins
Sapphire plugins for KBot. Contribute to KBot-discord/plugins development by creating an account on GitHub.
Aaron
Aaron5mo ago
I might be confused on how this helps, but I don't see how this has anything to do with it skipping over the command file? It is clearly already attempting to load the command at the right path, but skipping over it for some reason. Also no idea what cogs are.
Favna
Favna5mo ago
Discord py term. Modules essentially. We get many discord py transitions here
Aaron
Aaron5mo ago
Oh I was just coming from detritus lol
Favna
Favna5mo ago
pretty sure dpy references the concept of cogs in a wheel/system but i never used dpy so idk
Aaron
Aaron5mo ago
In detritus you could register commands in the options field it had and it was basically a command of itself. It just made it a subcommand So I instinctivly did ModGroup cause I was expecting to be able to separate my sub commands into their own files.
Favna
Favna5mo ago
anyway did you change src/pieces to dist/pieces already?
Aaron
Aaron5mo ago
I can right now yea.
Favna
Favna5mo ago
You're loading the JS files after all, not the TS ones
Aaron
Aaron5mo ago
yea thats fair. Idk what I was thinking there. Haven't made a discord bot in 2.5 years KEKW
Favna
Favna5mo ago
Might I say I've never heard of detritus before but I find it extremely ironic how they first say they are a "pure-TypeScript library" but then proceed to use requirement statements in the top most code sample
Aaron
Aaron5mo ago
lol they are a dead project now. but it was actually a refreshing library rather than djs
kyra
kyra5mo ago
That being said, the file structure you're using doesn't align with what the JS ecosystem typically does, which is why you're going thru some hops to even get it working 😅 In the JS ecosystem we have the piece directories alongside a lib directory, which is what your core folder is
Aaron
Aaron5mo ago
Yea, I haven't used typescript in a while, I just have never made a bot in rust panicc
Favna
Favna5mo ago
Adding to what kyra said, Maybe also have a looksie at some of the bots using sapphire
Spinel
Spinel5mo ago
Discord Bots using @sapphire/framework v5.x - Gemboard ᴱ ᴰ - Skyra ᴱ ᴬ ᴰ - Dragonite ᴱ ᴰ - Archangel ᴱ ᴰ - Official Bot Examples ᴱ ᴰ ᴶˢ - KBot ᴱ ᴬ ᴰ v4.x - Radon ᴱ ᴬ - Sapphire Application Commands Examples ᴱ - Zeyr ᴰ ᴬ - Birthdayy ᴰ - RTByte ᴱ ᴬ Legend for the identifiers : Uses ESM (if not specified then uses CJS) : Advanced bot (if not specified it is a simple bot, or not graded) : Uses Docker in production ᴶˢ: Written in JavaScript. If not specified then the bot is written in TypeScript.
Favna
Favna5mo ago
I just realised that there's only 2 (ish) CJS bots there @kyra 🩵🩷🤍🩷🩵
kyra
kyra5mo ago
lol, you're right
Favna
Favna5mo ago
ESM stay winning
Aaron
Aaron5mo ago
Also as for the other question I had, do i really have to code my subcommands all in one file?
kyra
kyra5mo ago
Adding to this, it works just fine without a performance impact because when you're registering a path in the store manager, you're actually registering the path + the store name, so registering src/pieces will register src/pieces/commands in the command store, src/pieces/listeners in the listener store, and so on. A src/pieces/lib would never be loaded unless you have a store named lib. If other loaders you used just loaded the entire root, I can see why you separated the "lib" from the "pieces".
Aaron
Aaron5mo ago
What is the point of the lib folder? I looked at some of those other projects and they don't seem to do much with it if they use it at all
kyra
kyra5mo ago
No, prefix non-piece utility files with _ (underscore) to ignore it in the loader. So if you have conf.ts, you can do _conf-add.ts, _conf-remove.ts, and so on, alternatively a _ prefixed directory would also work: _conf/add.ts, _conf/remove.ts.
Aaron
Aaron5mo ago
Okay, but how do I load those as sub commands ?
kyra
kyra5mo ago
You'd export a function in those files which then the command calls in the subcommands
Aaron
Aaron5mo ago
gotcha so it'd work similar I just have to import that function and use the function name
Favna
Favna5mo ago
ish. If you use the subcommands plugin then yes you do need to at least define the subcommands in the same file, but you can implement a class like this (rapid pseudo code!):
import { handleA } from '_handleA.js';
import { ApplyOptions } from '@sapphire/decorators';
import { Subcommand } from '@sapphire/plugin-subcommands';


@ApplyOptions<Subcommand.Options>({
subcommands: [
{
name: 'a',
chatInputRun: 'doA';
}
]
})
export class MyCommand extends Subcommand {
public doA(interaction: thetype) {
return handleA(interaction)
}
}
import { handleA } from '_handleA.js';
import { ApplyOptions } from '@sapphire/decorators';
import { Subcommand } from '@sapphire/plugin-subcommands';


@ApplyOptions<Subcommand.Options>({
subcommands: [
{
name: 'a',
chatInputRun: 'doA';
}
]
})
export class MyCommand extends Subcommand {
public doA(interaction: thetype) {
return handleA(interaction)
}
}
// _handleA.ts
import { container } from '@sapphire/framework';

export function handleA(interaction: thetype) {
// Do something cool
container.client.something;
}
// _handleA.ts
import { container } from '@sapphire/framework';

export function handleA(interaction: thetype) {
// Do something cool
container.client.something;
}
kyra
kyra5mo ago
Yup, you can use container from @sapphire/framework if you need to access to the Client instance of any other part of your application. In fact, this.container in pieces is actually a getter returning that object.
Aaron
Aaron5mo ago
Well thanks for all the help guys! I seem to be understanding more of how sapphire works