Skip to content

Commit 30791c0

Browse files
committed
feat: legacy plugin templates
1 parent 71cd12e commit 30791c0

File tree

4 files changed

+379
-245
lines changed

4 files changed

+379
-245
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default async function onGuildMemberAdd() {
2+
console.log('guildMemberAdd event fired!');
3+
};

packages/legacy/src/cli.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { CompilerPlugin, CompilerPluginRuntime, Logger } from 'commandkit';
2+
import { LegacyHandlerPluginOptions } from './plugin.js';
3+
import { join } from 'path';
4+
import { existsSync, mkdirSync } from 'fs';
5+
import { writeFile } from 'fs/promises';
6+
7+
export class LegacyCommandsCLIPlugin extends CompilerPlugin<LegacyHandlerPluginOptions> {
8+
public readonly name = 'LegacyCommandsCLIPlugin';
9+
10+
public async activate(ctx: CompilerPluginRuntime): Promise<void> {
11+
ctx.registerTemplate('legacy', this.handleTemplate.bind(this));
12+
}
13+
14+
public async deactivate(ctx: CompilerPluginRuntime): Promise<void> {
15+
ctx.unregisterTemplate('legacy');
16+
}
17+
18+
private panic(message: string): never {
19+
Logger.error(message);
20+
process.exit(1);
21+
}
22+
23+
private getCommandSource(name: string, isTypeScript: boolean) {
24+
if (isTypeScript) {
25+
return `import type { CommandData, SlashCommandProps } from '@commandkit/legacy';
26+
27+
export const data: CommandData = {
28+
name: '${name}',
29+
description: '${name} command',
30+
};
31+
32+
export async function run({ interaction }: SlashCommandProps) {
33+
await interaction.reply('Hello from ${name} command!');
34+
}
35+
`;
36+
}
37+
38+
return `/**
39+
* @type {import('@commandkit/legacy').CommandData}
40+
*/
41+
export const command = {
42+
name: '${name}',
43+
description: '${name} command',
44+
};
45+
46+
/**
47+
* @type {import('@commandkit/legacy').SlashCommandProps}
48+
*/
49+
export async function run({ interaction }: SlashCommandProps) {
50+
await interaction.reply('Hello from ${name} command!');
51+
}
52+
`;
53+
}
54+
55+
private async handleTemplate(args: string[]): Promise<void> {
56+
const message = `Invalid arguments for legacy template. Expected: <command|event> <name>`;
57+
58+
if (args.length < 2) {
59+
this.panic(message);
60+
}
61+
62+
const isTypeScript = existsSync(join(process.cwd(), 'tsconfig.json'));
63+
const extension = isTypeScript ? 'ts' : 'js';
64+
65+
const [type, name] = args;
66+
67+
switch (type) {
68+
case 'command': {
69+
const commandPath = join(
70+
process.cwd(),
71+
'src',
72+
this.options.commandsPath,
73+
`${name}.${extension}`,
74+
);
75+
76+
if (existsSync(commandPath)) {
77+
this.panic(`Command ${name} already exists.`);
78+
}
79+
80+
await writeFile(commandPath, this.getCommandSource(name, isTypeScript));
81+
82+
return Logger.info(
83+
`Command ${name} created successfully at ${commandPath}.`,
84+
);
85+
}
86+
case 'event': {
87+
let fileName = `${name}.${extension}`;
88+
const eventPathDir = join(
89+
process.cwd(),
90+
'src',
91+
this.options.eventsPath,
92+
name,
93+
);
94+
95+
if (!existsSync(eventPathDir)) {
96+
mkdirSync(eventPathDir, { recursive: true });
97+
}
98+
99+
let iter = 1;
100+
while (true) {
101+
if (!existsSync(`${eventPathDir}/${fileName}`)) break;
102+
fileName = `${name}-${iter}.${extension}`;
103+
iter++;
104+
}
105+
106+
const eventPath = join(eventPathDir, fileName);
107+
108+
await writeFile(
109+
eventPath,
110+
`export default async function on${name[0].toUpperCase() + name.slice(1)}() {\n console.log('${name} event fired!');\n};`,
111+
);
112+
113+
return Logger.info(
114+
`Event ${name} created successfully at ${eventPath}.`,
115+
);
116+
}
117+
default:
118+
this.panic(message);
119+
}
120+
}
121+
}

packages/legacy/src/index.ts

Lines changed: 13 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -1,248 +1,10 @@
1-
import {
2-
CommandKitPluginRuntime,
3-
Logger,
4-
RuntimePlugin,
5-
getCurrentDirectory,
6-
MiddlewareContext,
7-
AutocompleteCommandContext,
8-
MessageContextMenuCommandContext,
9-
UserContextMenuCommandContext,
10-
LoadedCommand,
11-
ChatInputCommandContext,
12-
CommandKitHMREvent,
13-
HMREventType,
14-
getSourceDirectories,
15-
CommandKitEventDispatch,
16-
} from 'commandkit';
17-
import { join, resolve } from 'node:path';
18-
import { loadLegacyValidations } from './loadLegacyValidations.js';
19-
import { CommandData, loadLegacyCommands } from './loadLegacyCommands.js';
20-
import { existsSync } from 'node:fs';
1+
import { LegacyCommandsCLIPlugin } from './cli.js';
2+
import { LegacyHandlerPlugin, LegacyHandlerPluginOptions } from './plugin.js';
213

22-
export interface LegacyHandlerPluginOptions {
23-
commandsPath: string;
24-
eventsPath: string;
25-
validationsPath: string;
26-
skipBuiltInValidations: boolean;
27-
devUserIds: string[];
28-
devGuildIds: string[];
29-
devRoleIds: string[];
30-
}
31-
32-
export class LegacyHandlerPlugin extends RuntimePlugin<LegacyHandlerPluginOptions> {
33-
public readonly name = 'LegacyHandlerPlugin';
34-
35-
private getPath(path: string): string {
36-
const basePath = getCurrentDirectory();
37-
return join(basePath, path);
38-
}
39-
40-
private getSourcePaths(path: string): string[] {
41-
const basePaths = getSourceDirectories();
42-
return basePaths.map((basePath) => join(basePath, path));
43-
}
44-
45-
private getCommandsPaths(): string[] {
46-
return this.getSourcePaths(this.options.commandsPath);
47-
}
48-
49-
private getEventsPaths(): string[] {
50-
return this.getSourcePaths(this.options.eventsPath);
51-
}
52-
53-
private getValidationsPaths(): string[] {
54-
return this.getSourcePaths(this.options.validationsPath);
55-
}
56-
57-
public async activate(ctx: CommandKitPluginRuntime): Promise<void> {
58-
Logger.info('LegacyHandlerPlugin activated');
59-
}
60-
61-
public async deactivate(ctx: CommandKitPluginRuntime): Promise<void> {
62-
Logger.info('LegacyHandlerPlugin deactivated');
63-
}
64-
65-
// TODO: properly handle hmr without reloading everything
66-
async performHMR(
67-
ctx: CommandKitPluginRuntime,
68-
event: CommandKitHMREvent,
69-
): Promise<void> {
70-
if (!event.path || !event.event) return;
71-
if (event.event !== HMREventType.Unknown) return;
72-
73-
const isCommand = this.getCommandsPaths().some((p) =>
74-
event.path.startsWith(p),
75-
);
76-
const isEvent = this.getEventsPaths().some((p) => event.path.startsWith(p));
77-
const isValidation = this.getValidationsPaths().some((p) =>
78-
event.path.startsWith(p),
79-
);
80-
81-
const isValid = isCommand || isEvent || isValidation;
82-
83-
if (!isValid) return;
84-
85-
// accept hmr handle
86-
event.preventDefault();
87-
event.accept();
88-
89-
if (isCommand || isValidation) {
90-
await ctx.commandkit.reloadCommands();
91-
} else if (isEvent) {
92-
await ctx.commandkit.reloadEvents();
93-
}
94-
}
95-
96-
public async onEventsRouterInit(ctx: CommandKitPluginRuntime): Promise<void> {
97-
const path = this.getPath(this.options.eventsPath);
98-
99-
if (!existsSync(path)) return;
100-
101-
ctx.commandkit.eventsRouter.addEntrypoints([path]);
102-
}
103-
104-
public async onBeforeCommandsLoad(
105-
ctx: CommandKitPluginRuntime,
106-
): Promise<void> {
107-
const middlewareIds = await this.loadValidations(ctx);
108-
await this.loadCommands(ctx, middlewareIds);
109-
}
110-
111-
public async willEmitEvent(
112-
ctx: CommandKitPluginRuntime,
113-
event: CommandKitEventDispatch,
114-
): Promise<void> {
115-
const eventPath = resolve(event.metadata.path);
116-
const ourPath = resolve(this.options.eventsPath);
117-
118-
if (!eventPath.startsWith(ourPath)) return;
119-
120-
event.accept();
121-
122-
// legacy handler injects the client to the last argument
123-
event.args.push(ctx.commandkit.client);
124-
}
125-
126-
private async loadCommands(
127-
ctx: CommandKitPluginRuntime,
128-
middlewareIds: string[],
129-
) {
130-
const commandsPath = this.getPath(this.options.commandsPath);
131-
if (!existsSync(commandsPath)) return;
132-
const commands = await loadLegacyCommands(commandsPath);
133-
134-
const commandHandler = ctx.commandkit.commandHandler;
135-
136-
for (const command of commands) {
137-
const data: LoadedCommand = {
138-
command: {
139-
category: command.category,
140-
id: crypto.randomUUID(),
141-
name: command.data.name,
142-
middlewares: middlewareIds,
143-
parentPath: commandsPath,
144-
path: command.path,
145-
relativePath: `legacy/${command.path}`,
146-
},
147-
data: {
148-
autocomplete: (ctx: AutocompleteCommandContext) =>
149-
command.autocomplete?.({
150-
client: ctx.client as any,
151-
interaction: ctx.interaction as any,
152-
handler: ctx.commandkit,
153-
}),
154-
messageContextMenu: command.messageContextMenu
155-
? (ctx: MessageContextMenuCommandContext) =>
156-
command.messageContextMenu?.({
157-
client: ctx.client as any,
158-
interaction: ctx.interaction as any,
159-
handler: ctx.commandkit,
160-
})
161-
: undefined,
162-
userContextMenu: command.messageContextMenu
163-
? (ctx: UserContextMenuCommandContext) =>
164-
command.userContextMenu?.({
165-
client: ctx.client as any,
166-
interaction: ctx.interaction as any,
167-
handler: ctx.commandkit,
168-
})
169-
: undefined,
170-
chatInput: (ctx: ChatInputCommandContext) =>
171-
command.chatInput?.({
172-
client: ctx.client as any,
173-
interaction: ctx.interaction as any,
174-
handler: ctx.commandkit,
175-
}),
176-
command: command.data,
177-
} as any,
178-
guilds: command.options?.guildOnly ? this.options.devGuildIds : [],
179-
};
180-
181-
(data.data as any).__command = command.options;
182-
183-
await commandHandler.registerExternalLoadedCommands([data]);
184-
}
185-
}
186-
187-
private async loadValidations(ctx: CommandKitPluginRuntime) {
188-
const validationsPath = this.getPath(this.options.validationsPath);
189-
if (!existsSync(validationsPath)) return [];
190-
191-
const validations = await loadLegacyValidations(
192-
validationsPath,
193-
this.options,
194-
this.options.skipBuiltInValidations,
195-
);
196-
197-
const commandHandler = ctx.commandkit.commandHandler;
198-
199-
const middlewareIds: string[] = [];
200-
201-
for (const validation of validations.validations) {
202-
const id = crypto.randomUUID();
203-
middlewareIds.push(id);
204-
await commandHandler.registerExternalLoadedMiddleware([
205-
{
206-
data: {
207-
async afterExecute(ctx: MiddlewareContext) {
208-
void ctx;
209-
return undefined;
210-
},
211-
async beforeExecute(ctx: MiddlewareContext) {
212-
const result = await validation.validate({
213-
client: ctx.client as any,
214-
interaction: ctx.interaction as any,
215-
commandObj: {
216-
category: ctx.command.command.category,
217-
filePath: ctx.command.command.path,
218-
data: ctx.command.data.command as CommandData,
219-
options: (ctx.command.data as any).__command || {},
220-
},
221-
handler: ctx.commandkit,
222-
});
223-
224-
if (result === true) return ctx.cancel();
225-
},
226-
} as any,
227-
middleware: {
228-
global: true,
229-
command: null,
230-
id,
231-
name: validation.name,
232-
parentPath: validationsPath,
233-
path: validation.path,
234-
relativePath: validation.path,
235-
},
236-
},
237-
]);
238-
}
239-
240-
return middlewareIds;
241-
}
242-
}
243-
244-
export function legacy(options?: Partial<LegacyHandlerPluginOptions>) {
245-
return new LegacyHandlerPlugin({
4+
export function legacy(
5+
options?: Partial<LegacyHandlerPluginOptions>,
6+
): [LegacyHandlerPlugin, LegacyCommandsCLIPlugin] {
7+
const opts = {
2468
commandsPath: './commands',
2479
eventsPath: './events',
24810
validationsPath: './validations',
@@ -251,9 +13,15 @@ export function legacy(options?: Partial<LegacyHandlerPluginOptions>) {
25113
devGuildIds: [],
25214
devRoleIds: [],
25315
...options,
254-
});
16+
};
17+
const plugin = new LegacyHandlerPlugin(opts);
18+
const cli = new LegacyCommandsCLIPlugin(opts);
19+
20+
return [plugin, cli];
25521
}
25622

23+
export * from './plugin.js';
25724
export * from './loadLegacyCommands.js';
25825
export * from './loadLegacyValidations.js';
25926
export * from './common.js';
27+
export * from './cli.js';

0 commit comments

Comments
 (0)