From 803afbec9021a66e1d7d30d8c94291e5167eac89 Mon Sep 17 00:00:00 2001 From: Johan Date: Sun, 15 Sep 2019 22:53:19 +0200 Subject: [PATCH] refactor: simplified commandParser --- package-lock.json | 54 ++++-- package.json | 1 + src/common/commandParser.ts | 222 ++++++++----------------- src/common/help.ts | 3 +- src/{models => types}/Command.ts | 2 +- src/{models => types}/CommandParser.ts | 4 +- src/types/global.d.ts | 1 + src/types/index.ts | 2 + test/common/commandParser.test.ts | 9 + 9 files changed, 126 insertions(+), 172 deletions(-) rename src/{models => types}/Command.ts (81%) rename src/{models => types}/CommandParser.ts (62%) create mode 100644 src/types/global.d.ts create mode 100644 src/types/index.ts create mode 100644 test/common/commandParser.test.ts diff --git a/package-lock.json b/package-lock.json index cb5f686..bfb42f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2990,6 +2990,16 @@ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, + "flexi-path": { + "version": "0.1.2-beta.3", + "resolved": "https://registry.npmjs.org/flexi-path/-/flexi-path-0.1.2-beta.3.tgz", + "integrity": "sha512-N/iXYINKZNtlNvY7elSQgB5yPJPMygaMgx1iXuZAsiLF56EnvefFyQuUnEQjYSK8JrFMFGQ90kauuz5aWduF7A==", + "requires": { + "@types/shelljs": "^0.8.5", + "shelljs": "^0.8.3", + "tslib": "^1.10.0" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -3047,7 +3057,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3068,12 +3079,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3088,17 +3101,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3215,7 +3231,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3227,6 +3244,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3241,6 +3259,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3248,12 +3267,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3272,6 +3293,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3352,7 +3374,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3364,6 +3387,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3449,7 +3473,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3485,6 +3510,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3504,6 +3530,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3547,12 +3574,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -7041,8 +7070,7 @@ "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tsutils": { "version": "3.17.1", diff --git a/package.json b/package.json index 598ada3..0983986 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/shelljs": "^0.8.5", "chalk": "^2.4.2", "extensionless": "^0.1.0", + "flexi-path": "^0.1.2-beta.3", "shelljs": "^0.8.3", "winston": "^3.2.1" }, diff --git a/src/common/commandParser.ts b/src/common/commandParser.ts index 7ad460d..7380298 100755 --- a/src/common/commandParser.ts +++ b/src/common/commandParser.ts @@ -1,22 +1,23 @@ #!/usr/bin/env node -import { join } from "path"; -import { existsSync, lstatSync, readdirSync } from "fs"; +import flexi, { until } from "flexi-path"; +import { + FlexiPath, + WalkedPath, + Path, + PathType +} from "flexi-path/dist/src/types"; import extensionless from "extensionless"; -import Command from "../models/Command"; -import CommandParser from "../models/CommandParser"; + +import { Command, CommandParser } from "../types"; import moduleLogger from "./logger"; -const ROOT_COMMAND_FOLDER = "../commands"; +const rootCommandPath = flexi + .path(__dirname) + .parent(x => x.name === "src") + .append("commands/"); const logger = moduleLogger.createLogger(module); -const scriptExtension = () => { - const scriptName = process.argv[1]; - - return scriptName.substring(scriptName.length - 2); -}; -const index = (): string => `index.${scriptExtension()}`; - const isHelp = (...args: string[]): boolean => { return (args.length > 0 && args[0] === "-h") || false; }; @@ -24,186 +25,97 @@ const isHelp = (...args: string[]): boolean => { const isDebug = (...args: string[]): boolean => args.filter(x => x === "--debug").length > 0; -const checkExists = (commandPath: string): boolean => { - const pathExists = existsSync(commandPath); - const pathWithExtExists = existsSync(`${commandPath}.${scriptExtension()}`); - - logger.debug(undefined, { - functionName: checkExists.name, - meta: { commandPath, pathExists, pathWithExtExists } +const mostSpecificCommand = (args: Path): WalkedPath => { + return flexi.walk.back(args, { + until: until.exists({ ignoreFileExtensions: true }) }); - - return pathExists || pathWithExtExists; }; -const buildCommandPath = (...args: string[]): string => { - const commandPath = join( - __dirname, - [ROOT_COMMAND_FOLDER].concat(args).join("/") - ); - - logger.debug(undefined, { - functionName: buildCommandPath.name, - meta: { args, commandPath } - }); - - return commandPath; -}; +const commandName = (args: Path): string => { + const { result } = mostSpecificCommand(args); -const getMostSpecificCommand = (...args: string[]): string[] => { - const commandPath = buildCommandPath(...args); + let { name } = result; - const exists = checkExists(commandPath) - ? args - : getMostSpecificCommand(...args.slice(0, -1)); - - logger.debug(undefined, { - functionName: getMostSpecificCommand.name, - meta: { args, commandPath, exists } - }); + if (result.type() === PathType.File && result.name === "index") { + name = result.parent().name; + } - return exists; + return extensionless(name); }; -const getCommandArgs = (...args: string[]): string[] => { - const commandArgs = getMostSpecificCommand(...args); +const commandFullName = (args: Path): string => { + const { result } = mostSpecificCommand(args); - return args.length > commandArgs.length ? args.slice(commandArgs.length) : []; + return result + .except(rootCommandPath) + .flatten() + .map(x => x.name) + .join(" "); }; -const getCommandName = (...args: string[]): string => { - const hit = getMostSpecificCommand(...args); - const commandPath = buildCommandPath(...hit); - const parts = commandPath.split("/"); +const requireContent = (path: FlexiPath): any | undefined => { + let modulePath: FlexiPath | undefined = path; + if (path.type() === PathType.Directory) { + modulePath = path.files().find(x => x.name === "index"); + } - return extensionless(parts[parts.length - 1]); -}; + if (modulePath !== undefined) { + // eslint-disable-next-line import/no-dynamic-require,global-require,@typescript-eslint/no-var-requires + return require(modulePath.path).default; + } -const getCommandFullName = (...args: string[]): string => { - return extensionless((getMostSpecificCommand(...args) || []).join(" ")); + return undefined; }; -const createCommand = ( - commandPath: string, - args: string[], - subCommands: Command[] -): Command | null => { - if (!commandPath || !lstatSync(commandPath).isFile()) { - return null; - } +const isCommand = (path: FlexiPath) => { + const { result } = mostSpecificCommand(path); + + return ( + result.type() === PathType.File || + result.files().find(x => x.name === "index") !== undefined + ); +}; - // eslint-disable-next-line import/no-dynamic-require,global-require,@typescript-eslint/no-var-requires - const content = require(commandPath).default; +const createCommand = (path: FlexiPath): Command => { + const { result, diff } = mostSpecificCommand(path); + const content = requireContent(result); return { - name: getCommandName(...args), - fullname: getCommandFullName(...args), + name: commandName(result), + fullname: commandFullName(result), run: content.run, helpname: content.helpname, description: content.description, - subCommands + args: diff.path.split(" "), + subCommands: result + .children() + .filter(x => isCommand(x)) + .map(x => createCommand(x)) }; }; -const command = (...args: string[]): Command | null => { - const deepestCommand = getMostSpecificCommand(...args); - const commandPath = buildCommandPath(...deepestCommand); - const commandPathExists = checkExists(commandPath); - const isDirectory = - existsSync(commandPath) && lstatSync(commandPath).isDirectory(); - - logger.debug(undefined, { - functionName: command.name, - meta: { deepestCommand, commandPath, commandPathExists, isDirectory } - }); - - if (commandPathExists === false) { - return null; - } - - let commandDefinition; - const subCommands: Command[] = []; - - if (isDirectory === false) { - const extension = scriptExtension(); - commandDefinition = commandPath; - if (commandDefinition && !commandDefinition.endsWith(extension)) { - commandDefinition = `${commandDefinition}.${extension}`; - } - } else if (isDirectory && checkExists(`${commandPath}/${index()}`)) { - commandDefinition = `${commandPath}/${index()}`; - readdirSync(commandPath) - .filter(x => { - const stat = lstatSync(`${commandPath}/${x}`); - - return stat.isFile() && x !== index() && !x.endsWith(".map"); - }) - .forEach(x => { - const temp = command(...args.concat([extensionless(x)])); - if (temp !== null) { - subCommands.push(temp); - } - }); - } - - let result: Command | null = null; - - if (commandDefinition !== undefined) { - if (checkExists(commandDefinition)) { - result = createCommand(commandDefinition, args, subCommands); - } - } - logger.debug(undefined, { - functionName: command.name, - meta: { commandDefinition, subCommands, result } - }); - - return result; -}; - -const getAvailableCommands = (): Command[] => { - const commandPath = buildCommandPath(); - - const commands: Command[] = []; - - readdirSync(commandPath) - .filter(x => !/.*\.map$/.test(x)) - .forEach(x => { - const temp = command(x); - if (temp !== null) { - commands.push(temp); - } - }); - - logger.debug(undefined, { - functionName: getAvailableCommands.name, - meta: { commandPath, commands } - }); - - return commands; +const allCommands = (): Command[] => { + return rootCommandPath + .children() + .filter(x => isCommand(x)) + .map(x => createCommand(x)); }; const withoutOptions = (...args: string[]): string[] => args.filter(x => !x.startsWith("-")); const parse = (...args: string[]): Command | null => { - const result = command(...args); - - if (result) { - result.args = getCommandArgs(...args); - } - - logger.debug(undefined, { functionName: parse.name, meta: { args, result } }); + const path = rootCommandPath.append(args); - return result; + return isCommand(path) ? createCommand(path) : null; }; const commandParser = (...args: string[]): CommandParser => { const commandArgs = [...(args || [])]; - const cleanArgs = withoutOptions(...args); // commandArgs.filter(x => !x.startsWith("-")); + const cleanArgs = withoutOptions(...args); return { - all: () => getAvailableCommands(), + all: () => allCommands(), find: () => parse(...cleanArgs), isHelp: isHelp(...commandArgs), isDebug: isDebug(...commandArgs) diff --git a/src/common/help.ts b/src/common/help.ts index 530d475..0ec73c3 100644 --- a/src/common/help.ts +++ b/src/common/help.ts @@ -2,7 +2,8 @@ import chalk from "chalk"; import moduleLogger from "./logger"; import parser from "./commandParser"; -import Command from "../models/Command"; + +import { Command } from "../types"; const logger = moduleLogger.createLogger(module); diff --git a/src/models/Command.ts b/src/types/Command.ts similarity index 81% rename from src/models/Command.ts rename to src/types/Command.ts index 8ded64f..4c4f40f 100644 --- a/src/models/Command.ts +++ b/src/types/Command.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -export default interface Command { +export interface Command { name: string; fullname: string; run: any; diff --git a/src/models/CommandParser.ts b/src/types/CommandParser.ts similarity index 62% rename from src/models/CommandParser.ts rename to src/types/CommandParser.ts index 34cbdfd..8601dba 100644 --- a/src/models/CommandParser.ts +++ b/src/types/CommandParser.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import Command from "./Command"; +import { Command } from "."; -export default interface CommandParser { +export interface CommandParser { all(): Command[]; find(args: string[]): Command | null; isHelp: boolean; diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..3b47093 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1 @@ +import "jest-extended"; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..7fb8f0a --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from "./Command"; +export * from "./CommandParser"; diff --git a/test/common/commandParser.test.ts b/test/common/commandParser.test.ts new file mode 100644 index 0000000..7f7f0e2 --- /dev/null +++ b/test/common/commandParser.test.ts @@ -0,0 +1,9 @@ +import commandParser from "../../src/common/commandParser"; + +describe("commandParser", () => { + describe("all", () => { + it("should return commands", () => { + expect(commandParser("").all().length).toBeGreaterThan(1); + }); + }); +});