-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
611 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}); |
Oops, something went wrong.