Skip to content

Commit

Permalink
feat(command-parser): basic impl
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite committed Jan 26, 2024
1 parent d5c5cfd commit 00320c1
Show file tree
Hide file tree
Showing 11 changed files with 611 additions and 8 deletions.
160 changes: 160 additions & 0 deletions packages/commandkit/helpers/command-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { dirname, join } from 'node:path';
import { readdir } from 'node:fs/promises';

export interface CommandParserOptions {
src: string;
filter: (name: string) => boolean;
maxDepth: number;
extensions: RegExp;
}

export const enum CommandEntityKind {
Command,
ContextMenuCommand,
Category,
Subcommand,
DynamicCommand,
Validator,
}

const IGNORE_PATTERN = /^_/;
const CATEGORY_PATTERN = /^\([a-z0-9-_]{1,}\)$/;
const DYNAMIC_PATTERN = /^\[[a-z0-9-_]{1,}\]$/;

export interface CommandEntity {
name: string;
kind: CommandEntityKind;
path: string;
children: CommandEntity[];
category: string | null;
}

export class CommandParser {
#data: CommandEntity[] = [];

public constructor(public readonly options: CommandParserOptions) {}

public clear() {
this.#data = [];
}

public getCommands() {
return this.#data;
}

public async scan() {
const { src, maxDepth } = this.options;

const files = await this.#scanDir(src, maxDepth);

this.#data.push(...files);

return files;
}

public map(): Map<string, CommandEntity> {
const map = new Map<string, CommandEntity>();

for (const command of this.#data) {
map.set(command.name, command);
}

return map;
}

async #scanDir(dir: string, depth: number) {
const files: CommandEntity[] = [];

for await (const dirent of await readdir(dir, { withFileTypes: true })) {
if (!this.options.filter(dirent.name)) continue;

if (dirent.isDirectory()) {
if (depth > 0) {
if (CATEGORY_PATTERN.test(dirent.name)) {
const name = dirent.name.replace(/\(|\)/g, '');
const fullPath = join(dir, dirent.name);

files.push({
name,
path: fullPath,
kind: CommandEntityKind.Category,
children: (await this.#scanDir(fullPath, depth - 1)).map((e) => ({
...e,
category: name,
})),
category: null,
});
} else {
const path = join(dir, dirent.name);

const nearestCategory =
path
.split('/')
.reverse()
.find((e) => CATEGORY_PATTERN.test(e)) ?? null;

// ignoring category pattern, if path is nested more than once, it is a subcommand
const isSubcommand =
path
.replace(this.options.src, '')
.split('/')
.filter((e) => !IGNORE_PATTERN.test(e)).length > 1;

const isDynamic = DYNAMIC_PATTERN.test(dirent.name);

files.push({
name: dirent.name,
kind: isSubcommand
? CommandEntityKind.Subcommand
: isDynamic
? CommandEntityKind.DynamicCommand
: CommandEntityKind.Command,
path,
children: [...(await this.#scanDir(path, depth - 1))],
category: nearestCategory,
});
}
}
} else {
if (!this.options.extensions.test(dirent.name)) continue;

const fullPath = join(dir, dirent.name);
const name = dirent.name.replace(this.options.extensions, '');

let kind: CommandEntityKind | null = null;

switch (name) {
case 'command':
kind = CommandEntityKind.Command;
break;
case 'validator':
kind = CommandEntityKind.Validator;
break;
case 'context':
kind = CommandEntityKind.ContextMenuCommand;
break;
default:
break;
}

if (kind === null) continue;

const nearestCategory =
fullPath
.split('/')
.reverse()
.find((e) => CATEGORY_PATTERN.test(e)) ?? null;

files.push({
name: dirname(fullPath),
path: fullPath,
kind,
children: [],
category: nearestCategory,
});
}
}

return files;
}
}
8 changes: 5 additions & 3 deletions packages/commandkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"build": "tsup",
"deploy": "npm publish",
"deploy-dev": "npm publish --access public --tag dev",
"test": "cd ./tests && node ../bin/index.mjs dev",
"test": "vitest",
"test:dev": "cd ./tests && node ../bin/index.mjs dev",
"test:build": "cd ./tests && node ../bin/index.mjs build",
"test:prod": "cd ./tests && node ../bin/index.mjs start"
},
Expand Down Expand Up @@ -52,9 +53,10 @@
"discord.js": "^14.14.1",
"tsconfig": "workspace:*",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^1.2.1"
},
"peerDependencies": {
"discord.js": "^14"
}
}
}
34 changes: 34 additions & 0 deletions packages/commandkit/spec/command-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, beforeAll, test, expect } from 'vitest';
import { CommandEntityKind, CommandParser } from '../helpers/command-parser';

describe('CommandParser', () => {
const parser = new CommandParser({
src: `${__dirname}/sample`,
filter: (name) => !name.startsWith('_'),
maxDepth: 5,
extensions: /\.(c|m)?(j|t)sx?$/,
});

beforeAll(async () => {
await parser.scan();
});

test('should scan all files', () => {
expect(parser.getCommands().length).toBeGreaterThan(0);
});

test('should contain general category', () => {
const cmds = parser.getCommands();
const cat = cmds.find((c) => c.kind === CommandEntityKind.Category && c.name === 'general');

expect(cat?.name).toBe('general');
});

test('general should contain commands', () => {
const cmds = parser.getCommands();
const cat = cmds.find((c) => c.kind === CommandEntityKind.Category && c.name === 'general');

expect(cat?.children.length).toBeGreaterThan(0);
expect(cat?.children.map((m) => m.name)).toEqual(['ping', 'pong']);
});
});
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
2 changes: 1 addition & 1 deletion packages/commandkit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "helpers/**/*.ts", "spec"],
"exclude": ["node_modules"]
}
9 changes: 9 additions & 0 deletions packages/commandkit/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
include: ['./spec/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
watch: false,
dangerouslyIgnoreUnhandledErrors: true,
},
});
Loading

0 comments on commit 00320c1

Please sign in to comment.