Skip to content

Commit ada80b1

Browse files
committed
feat(core): initial checkin of core code
1 parent eb6adc0 commit ada80b1

8 files changed

Lines changed: 1583 additions & 20 deletions

File tree

package-lock.json

Lines changed: 1265 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
"build": "tsc -p ./",
88
"clean": "rimraf ./dist",
99
"commit": "commit",
10+
"preexample": "npm run package",
11+
"example": "ts-node ./example/cli.ts",
1012
"lint": "tslint -p ./tsconfig.json",
1113
"lint:fix": "npm run lint -- --fix",
1214
"prebuild": "npm run clean",
15+
"prepackage": "npm run build",
16+
"package": "ts-node ./support/package.ts",
1317
"test": "echo \"Error: no test specified\" && exit 1"
1418
},
19+
"private": true,
1520
"author": "Ben <codeandcats@gmail.com>",
1621
"license": "ISC",
1722
"husky": {
@@ -23,8 +28,18 @@
2328
"@commitlint/cli": "^7.1.2",
2429
"@commitlint/config-conventional": "^7.1.2",
2530
"@commitlint/prompt-cli": "^7.1.2",
31+
"@types/app-root-path": "^1.2.4",
32+
"@types/fs-extra": "^5.0.4",
2633
"rimraf": "^2.6.2",
34+
"ts-loader": "^5.0.0",
35+
"ts-node": "^7.0.1",
2736
"tslint": "^5.11.0",
2837
"typescript": "^3.0.3"
38+
},
39+
"dependencies": {
40+
"app-root-path": "^2.1.0",
41+
"chalk": "^2.4.1",
42+
"fs-extra": "^7.0.0",
43+
"reflect-metadata": "^0.1.12"
2944
}
3045
}

src/commander.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// tslint:disable:no-console
2+
import * as root from 'app-root-path';
3+
import chalk from 'chalk';
4+
import * as cli from 'commander';
5+
import * as fs from 'fs-extra';
6+
import { getCommandOptions, getCommands, getCommandValues } from './metadata';
7+
import { Command, CommandDefinition, CommandOptionDefinition, CommandValueDefinition, IocContainer } from './types';
8+
9+
export class Commander {
10+
private iocContainer?: IocContainer;
11+
private _version?: string;
12+
private isCommanderInitialised = false;
13+
14+
public ioc(iocContainer: IocContainer): this {
15+
this.iocContainer = iocContainer;
16+
return this;
17+
}
18+
19+
public version(version: string): this {
20+
this._version = version;
21+
return this;
22+
}
23+
24+
public async execute(argv?: string[]): Promise<void> {
25+
await this.initialiseCommander();
26+
27+
const args = argv || process.argv;
28+
29+
if (args.length <= 2) {
30+
cli.help();
31+
}
32+
33+
cli.parse(args);
34+
}
35+
36+
private async initialiseCommander() {
37+
if (this.isCommanderInitialised) {
38+
return;
39+
}
40+
await this.setVersion();
41+
this.registerCommands();
42+
this.isCommanderInitialised = true;
43+
}
44+
45+
private async getPackageVersion(): Promise<string | undefined> {
46+
const packageFileName = root.resolve('package.json');
47+
if (!await fs.pathExists(packageFileName)) {
48+
return undefined;
49+
}
50+
51+
const packageObject = await fs.readJSON(packageFileName);
52+
53+
return packageObject && packageObject.version;
54+
}
55+
56+
private async setVersion() {
57+
const version =
58+
this._version !== undefined ?
59+
this._version :
60+
await this.getPackageVersion();
61+
62+
if (version) {
63+
cli.version(version);
64+
}
65+
}
66+
67+
private getValuesUsage(values: CommandValueDefinition[]) {
68+
return values
69+
.map((param) => param.optional ? `[${param.name}]` : `<${param.name}>`)
70+
.join(' ');
71+
}
72+
73+
private getOptionUsage(option: CommandOptionDefinition): string {
74+
let result = '';
75+
76+
if (option.shortName) {
77+
result = `-${option.shortName}`;
78+
}
79+
80+
result += ` --${option.name.toString()}`;
81+
82+
if (option.valueName) {
83+
result += ` <${option.valueName}>`;
84+
}
85+
86+
return result.trim();
87+
}
88+
89+
private registerCommandOption(cliCommand: cli.Command, paramsClass: { new(...args: any[]): any }, option: CommandOptionDefinition) {
90+
const optionUsage = this.getOptionUsage(option);
91+
const coerceValue = !option.valueName ? undefined : ((value: string) => {
92+
if (option.valueName && option.type === Number) {
93+
return +value;
94+
} else {
95+
return value;
96+
}
97+
});
98+
99+
const params = new paramsClass();
100+
const defaultValue = params[option.name];
101+
102+
cliCommand.option(
103+
optionUsage,
104+
option.description,
105+
coerceValue,
106+
defaultValue
107+
);
108+
}
109+
110+
private getParams(
111+
command: CommandDefinition<any>,
112+
args: any[]
113+
) {
114+
const values = getCommandValues(command.paramsClass.prototype);
115+
const options = getCommandOptions(command.paramsClass.prototype);
116+
117+
const params: { [paramName: string]: any } = new command.paramsClass();
118+
let paramIndex = 0;
119+
120+
for (const value of values) {
121+
params[value.name] = args[paramIndex++];
122+
}
123+
124+
const optionValues = args[paramIndex];
125+
for (const option of options) {
126+
params[option.name.toString()] = optionValues[option.name];
127+
if (option.shortName) {
128+
params[option.shortName] = optionValues[option.shortName];
129+
}
130+
}
131+
132+
return params;
133+
}
134+
135+
private instantiateCommand(command: CommandDefinition<any>) {
136+
const constructor = command.type as { new(...args: any[]): Command<any> };
137+
const instance = this.iocContainer ? this.iocContainer.get(constructor) : new constructor();
138+
return instance;
139+
}
140+
141+
private registerCommand(command: CommandDefinition<any>) {
142+
const values = getCommandValues(command.paramsClass.prototype);
143+
const usage = `${command.name} ${this.getValuesUsage(values)}`;
144+
const cliCommand = cli.command(usage);
145+
146+
if (command.description) {
147+
cliCommand.description(command.description);
148+
}
149+
150+
const options = getCommandOptions(command.paramsClass.prototype);
151+
for (const option of options) {
152+
this.registerCommandOption(cliCommand, command.paramsClass, option);
153+
}
154+
155+
cliCommand.action(async (...args: any[]) => {
156+
try {
157+
const commandInstance = this.instantiateCommand(command);
158+
const params = this.getParams(command, args);
159+
await commandInstance.execute(params);
160+
} catch (err) {
161+
console.error();
162+
console.error(chalk.red(err.stack || err.message || err));
163+
console.error();
164+
process.exit(1);
165+
}
166+
});
167+
}
168+
169+
private registerCommands() {
170+
const commands = getCommands();
171+
172+
for (const command of commands) {
173+
this.registerCommand(command);
174+
}
175+
}
176+
}

src/decorators.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as meta from './metadata';
2+
import { CommandOptionDefinitionOptions } from './types';
3+
4+
export const command = (name: string, paramsClass: { new(): {} }, description?: string) => (target: any) => {
5+
meta.addCommand({ name, type: target, paramsClass, description });
6+
};
7+
8+
export const option = (options?: CommandOptionDefinitionOptions) => ((target: any, name: string) => {
9+
meta.addCommandOption(target, name, options);
10+
});
11+
12+
export const value = (options?: { optional?: boolean }) => ((target: any, name: string) => {
13+
meta.addCommandValue(target, {
14+
name,
15+
optional: !!(options && options.optional)
16+
});
17+
});

src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Commander } from './commander';
2+
import { IocContainer } from './types';
3+
4+
export { command, option, value } from './decorators';
5+
export { Command, CommandValueDefinition, CommandOptionDefinition, CommandOptionDefinitionOptions, IocContainer } from './types';
6+
7+
const commander = new Commander();
8+
9+
// tslint:disable-next-line:no-shadowed-variable
10+
export function version(version: string): Commander {
11+
return commander.version(version);
12+
}
13+
14+
export function ioc(container: IocContainer): Commander {
15+
return commander.ioc(container);
16+
}
17+
18+
export async function execute(argv?: string[]): Promise<void> {
19+
return commander.execute(argv);
20+
}

src/metadata.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import 'reflect-metadata';
2+
3+
import { CommandDefinition, CommandOptionDefinition, CommandOptionDefinitionOptions, CommandValueDefinition } from './types';
4+
5+
const metadataKeys = Object.freeze({
6+
command: Symbol('command'),
7+
options: Symbol('options'),
8+
values: Symbol('values')
9+
});
10+
11+
const commandDefinitions: Array<CommandDefinition<any>> = [];
12+
13+
export function getCommandValues(paramsClassPrototype: any): CommandValueDefinition[] {
14+
return [...(Reflect.getOwnMetadata(metadataKeys.values, paramsClassPrototype) || [])];
15+
}
16+
17+
export function addCommandValue(paramsClassPrototype: any, options: Pick<CommandValueDefinition, 'name' | 'optional'>) {
18+
const type = Reflect.getMetadata('design:type', paramsClassPrototype, options.name);
19+
const values = getCommandValues(paramsClassPrototype);
20+
values.push({ ...options, type });
21+
Reflect.defineMetadata(metadataKeys.values, values, paramsClassPrototype);
22+
}
23+
24+
export function getCommandOptions(paramsClassPrototype: any): CommandOptionDefinition[] {
25+
return [...(Reflect.getOwnMetadata(metadataKeys.options, paramsClassPrototype) || [])];
26+
}
27+
28+
export function addCommandOption(paramsClassPrototype: any, name: string, options: CommandOptionDefinitionOptions | undefined) {
29+
const type = Reflect.getMetadata('design:type', paramsClassPrototype, name);
30+
31+
const allOptions = getCommandOptions(paramsClassPrototype);
32+
allOptions.push({
33+
name,
34+
type,
35+
...options
36+
});
37+
Reflect.defineMetadata(metadataKeys.options, allOptions, paramsClassPrototype);
38+
}
39+
40+
export function addCommand(command: CommandDefinition<any>) {
41+
commandDefinitions.push(command);
42+
}
43+
44+
export function getCommands(): Array<CommandDefinition<any>> {
45+
return [...commandDefinitions];
46+
}

src/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export interface CommandValueDefinition {
2+
name: string;
3+
type: typeof String | typeof Number | typeof Boolean;
4+
optional: boolean;
5+
}
6+
7+
export interface CommandOptionDefinition {
8+
name: string;
9+
shortName?: string;
10+
description?: string;
11+
type: typeof String | typeof Number;
12+
valueName?: string;
13+
}
14+
15+
export interface CommandOptionDefinitionOptions {
16+
shortName?: string;
17+
description?: string;
18+
valueName?: string;
19+
}
20+
21+
export interface CommandDefinition<TCommand> {
22+
name: string;
23+
type: { new(...args: any[]): TCommand };
24+
paramsClass: { new(): any };
25+
description?: string;
26+
}
27+
28+
export interface Command<TParams> {
29+
execute(params: TParams): Promise<void>;
30+
}
31+
32+
export interface IocContainer {
33+
get: <T>(Class: { new(...args: any[]): T }) => T;
34+
}

tsconfig.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
{
22
"compilerOptions": {
33
"declaration": true,
4+
"emitDecoratorMetadata": true,
5+
"experimentalDecorators": true,
6+
"lib": [
7+
"es2015"
8+
],
49
"module": "commonjs",
10+
"moduleResolution": "node",
511
"outDir": "./dist",
612
"skipLibCheck": true,
713
"strict": true
814
},
915
"files": [
1016
"./src/index.ts"
17+
],
18+
"exclude": [
19+
"./node_modules"
1120
]
12-
}
21+
}

0 commit comments

Comments
 (0)