@rsdk/cli.core 5.7.0-next.2 → 6.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +1 -1
- package/dist/cli.d.ts +17 -19
- package/dist/cli.js +37 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/loaders/implementations/index.d.ts +2 -0
- package/dist/loaders/implementations/index.js +19 -0
- package/dist/loaders/implementations/index.js.map +1 -0
- package/dist/loaders/implementations/local-path.loader.d.ts +9 -0
- package/dist/loaders/implementations/local-path.loader.js +49 -0
- package/dist/loaders/implementations/local-path.loader.js.map +1 -0
- package/dist/loaders/implementations/pkg.loader.d.ts +11 -0
- package/dist/loaders/implementations/pkg.loader.js +31 -0
- package/dist/loaders/implementations/pkg.loader.js.map +1 -0
- package/dist/loaders/index.d.ts +1 -0
- package/dist/loaders/index.js +18 -0
- package/dist/loaders/index.js.map +1 -0
- package/dist/loaders/loader.interface.d.ts +5 -0
- package/dist/loaders/loader.interface.js +3 -0
- package/dist/loaders/loader.interface.js.map +1 -0
- package/dist/resolvers/command.resolver.d.ts +27 -0
- package/dist/resolvers/command.resolver.js +81 -0
- package/dist/resolvers/command.resolver.js.map +1 -0
- package/dist/resolvers/options.resolver.d.ts +35 -0
- package/dist/resolvers/options.resolver.js +80 -0
- package/dist/resolvers/options.resolver.js.map +1 -0
- package/jest.config.js +1 -0
- package/jest.config.unit.js +1 -0
- package/package.json +19 -6
- package/src/cli.ts +52 -38
- package/src/index.ts +11 -3
- package/src/loaders/implementations/index.ts +2 -0
- package/src/loaders/implementations/local-path.loader.ts +53 -0
- package/src/loaders/implementations/pkg.loader.ts +33 -0
- package/src/loaders/index.ts +1 -0
- package/src/loaders/loader.interface.ts +6 -0
- package/src/resolvers/command.resolver.ts +103 -0
- package/src/resolvers/options.resolver.ts +92 -0
- package/test/cli.spec.ts +131 -0
- package/test/commands/command.single.ts +30 -0
- package/test/commands/command.with-all.ts +93 -0
- package/test/commands/command.with-subcommands.ts +55 -0
- package/test/commands/command.with-variations.ts +55 -0
- package/test/commands/js/command.single.js +47 -0
- package/test/commands-resolver.spec.ts +125 -0
- package/test/loaders.spec.ts +61 -0
- package/test/options-resolver.spec.ts +119 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Command,
|
|
3
|
+
CommandVariation,
|
|
4
|
+
IRunnable,
|
|
5
|
+
Option,
|
|
6
|
+
ValueOption,
|
|
7
|
+
SubCommand,
|
|
8
|
+
} from '@rsdk/cli.common';
|
|
9
|
+
import { StringParser } from '@rsdk/common';
|
|
10
|
+
import { LoggerFactory } from '@rsdk/logging';
|
|
11
|
+
|
|
12
|
+
@SubCommand('first-subcommand', {
|
|
13
|
+
description: 'First subcommand example',
|
|
14
|
+
})
|
|
15
|
+
class FirstSubCommand implements IRunnable {
|
|
16
|
+
async run(
|
|
17
|
+
@Option('flag', { description: 'test flag option' })
|
|
18
|
+
flag: boolean,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const logger = LoggerFactory.create(FirstSubCommand);
|
|
21
|
+
|
|
22
|
+
logger.info(`First subcommand executed with flag: ${flag}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@SubCommand('second-subcommand', {
|
|
27
|
+
description: 'Second subcommand example',
|
|
28
|
+
})
|
|
29
|
+
class SecondSubCommand implements IRunnable {
|
|
30
|
+
async run(
|
|
31
|
+
@ValueOption('value', new StringParser(), {
|
|
32
|
+
description: 'test value option',
|
|
33
|
+
defaultValue: 'test',
|
|
34
|
+
})
|
|
35
|
+
value: string,
|
|
36
|
+
|
|
37
|
+
@Option('flag', { description: 'test flag option' })
|
|
38
|
+
flag: boolean,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const logger = LoggerFactory.create(SecondSubCommand);
|
|
41
|
+
|
|
42
|
+
logger.info(
|
|
43
|
+
`Second subcommand executed with value: ${value} and flag: ${flag}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@CommandVariation('with-variations:first', {
|
|
49
|
+
description: 'First variation example',
|
|
50
|
+
})
|
|
51
|
+
class FirstVariation implements IRunnable {
|
|
52
|
+
async run(
|
|
53
|
+
@Option('flag', { description: 'test flag option' })
|
|
54
|
+
flag: boolean,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const logger = LoggerFactory.create(WithAll);
|
|
57
|
+
|
|
58
|
+
logger.info(`First variation executed with flag: ${flag}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@CommandVariation('with-variations:second', {
|
|
63
|
+
description: 'Second variation example',
|
|
64
|
+
})
|
|
65
|
+
class SecondVariation implements IRunnable {
|
|
66
|
+
async run(
|
|
67
|
+
@ValueOption('value', new StringParser(), {
|
|
68
|
+
description: 'test value option',
|
|
69
|
+
defaultValue: 'test',
|
|
70
|
+
})
|
|
71
|
+
value: string,
|
|
72
|
+
|
|
73
|
+
@Option('flag', { description: 'test flag option' })
|
|
74
|
+
flag: boolean,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const logger = LoggerFactory.create(WithAll);
|
|
77
|
+
|
|
78
|
+
logger.info(
|
|
79
|
+
`Second variation executed with value: ${value} and flag: ${flag}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Command('with-all', {
|
|
85
|
+
description: 'Test command with subcommands and variations',
|
|
86
|
+
subcommands: [FirstSubCommand, SecondSubCommand],
|
|
87
|
+
variations: [FirstVariation, SecondVariation],
|
|
88
|
+
})
|
|
89
|
+
export default class WithAll implements IRunnable {
|
|
90
|
+
async run(): Promise<void> {
|
|
91
|
+
console.log('Main command executed');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Command,
|
|
3
|
+
IRunnable,
|
|
4
|
+
Option,
|
|
5
|
+
ValueOption,
|
|
6
|
+
SubCommand,
|
|
7
|
+
} from '@rsdk/cli.common';
|
|
8
|
+
import { StringParser } from '@rsdk/common';
|
|
9
|
+
import { LoggerFactory } from '@rsdk/logging';
|
|
10
|
+
|
|
11
|
+
@SubCommand('first', {
|
|
12
|
+
description: 'First subcommand example',
|
|
13
|
+
})
|
|
14
|
+
class FirstSubCommand implements IRunnable {
|
|
15
|
+
async run(
|
|
16
|
+
@Option('flag', { description: 'test flag option' })
|
|
17
|
+
flag: boolean,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const logger = LoggerFactory.create(WithSubCommands);
|
|
20
|
+
|
|
21
|
+
logger.info(`First subcommand executed with flag: ${flag}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@SubCommand('subcommand-1', {
|
|
26
|
+
description: 'Second subcommand example',
|
|
27
|
+
})
|
|
28
|
+
class SecondSubCommand implements IRunnable {
|
|
29
|
+
async run(
|
|
30
|
+
@ValueOption('value', new StringParser(), {
|
|
31
|
+
description: 'test value option',
|
|
32
|
+
defaultValue: 'test',
|
|
33
|
+
})
|
|
34
|
+
value: string,
|
|
35
|
+
|
|
36
|
+
@Option('flag', { description: 'test flag option' })
|
|
37
|
+
flag: boolean,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const logger = LoggerFactory.create(WithSubCommands);
|
|
40
|
+
|
|
41
|
+
logger.info(
|
|
42
|
+
`Second subcommand executed with value: ${value} and flag: ${flag}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Command('with-subcommands', {
|
|
48
|
+
description: 'Test command with subcommands',
|
|
49
|
+
subcommands: [FirstSubCommand, SecondSubCommand],
|
|
50
|
+
})
|
|
51
|
+
export default class WithSubCommands implements IRunnable {
|
|
52
|
+
async run(): Promise<void> {
|
|
53
|
+
console.log('Main command executed');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Command,
|
|
3
|
+
CommandVariation,
|
|
4
|
+
IRunnable,
|
|
5
|
+
Option,
|
|
6
|
+
ValueOption,
|
|
7
|
+
} from '@rsdk/cli.common';
|
|
8
|
+
import { StringParser } from '@rsdk/common';
|
|
9
|
+
import { LoggerFactory } from '@rsdk/logging';
|
|
10
|
+
|
|
11
|
+
@CommandVariation('with-variations:first', {
|
|
12
|
+
description: 'First variation example',
|
|
13
|
+
})
|
|
14
|
+
class FirstVariation implements IRunnable {
|
|
15
|
+
async run(
|
|
16
|
+
@Option('flag', { description: 'test flag option' })
|
|
17
|
+
flag: boolean,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const logger = LoggerFactory.create(WithVariations);
|
|
20
|
+
|
|
21
|
+
logger.info(`First subcommand executed with flag: ${flag}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@CommandVariation('with-variations:second', {
|
|
26
|
+
description: 'Second variation example',
|
|
27
|
+
})
|
|
28
|
+
class SecondVariation implements IRunnable {
|
|
29
|
+
async run(
|
|
30
|
+
@ValueOption('value', new StringParser(), {
|
|
31
|
+
description: 'test value option',
|
|
32
|
+
defaultValue: 'test',
|
|
33
|
+
})
|
|
34
|
+
value: string,
|
|
35
|
+
|
|
36
|
+
@Option('flag', { description: 'test flag option' })
|
|
37
|
+
flag: boolean,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const logger = LoggerFactory.create(WithVariations);
|
|
40
|
+
|
|
41
|
+
logger.info(
|
|
42
|
+
`Second subcommand executed with value: ${value} and flag: ${flag}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Command('with-variations', {
|
|
48
|
+
description: 'Test command with subcommands',
|
|
49
|
+
variations: [FirstVariation, SecondVariation],
|
|
50
|
+
})
|
|
51
|
+
export default class WithVariations implements IRunnable {
|
|
52
|
+
async run(): Promise<void> {
|
|
53
|
+
console.log('Main command executed');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* eslint-disable unicorn/no-abusive-eslint-disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
13
|
+
};
|
|
14
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
15
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
16
|
+
};
|
|
17
|
+
var SingleCommand_1;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
const cli_common_1 = require("@rsdk/cli.common");
|
|
20
|
+
const common_1 = require("@rsdk/common");
|
|
21
|
+
const logging_1 = require("@rsdk/logging");
|
|
22
|
+
let SingleCommand = SingleCommand_1 = class SingleCommand {
|
|
23
|
+
async execute(message, verbose) {
|
|
24
|
+
const logger = logging_1.LoggerFactory.create(SingleCommand_1);
|
|
25
|
+
if (verbose) {
|
|
26
|
+
logger.info('Command parameters:');
|
|
27
|
+
logger.info(`message: ${message}`);
|
|
28
|
+
logger.info(`verbose: ${verbose}`);
|
|
29
|
+
}
|
|
30
|
+
logger.info(message);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
__decorate([
|
|
34
|
+
__param(0, (0, cli_common_1.ValueOption)('message', new common_1.StringParser(), {
|
|
35
|
+
description: 'Message to log',
|
|
36
|
+
defaultValue: 'Hello World!',
|
|
37
|
+
alias: 'm',
|
|
38
|
+
})),
|
|
39
|
+
__param(1, (0, cli_common_1.Option)('verbose', { description: 'Verbose output' })),
|
|
40
|
+
__metadata("design:type", Function),
|
|
41
|
+
__metadata("design:paramtypes", [String, Boolean]),
|
|
42
|
+
__metadata("design:returntype", Promise)
|
|
43
|
+
], SingleCommand.prototype, "execute", null);
|
|
44
|
+
SingleCommand = SingleCommand_1 = __decorate([
|
|
45
|
+
(0, cli_common_1.Command)('single-command', 'Test command that logs parameters')
|
|
46
|
+
], SingleCommand);
|
|
47
|
+
exports.default = SingleCommand;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { MetadataRegistry } from '@rsdk/cli.common';
|
|
2
|
+
import type { ILogger } from '@rsdk/logging';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
import { CommandResolver } from '../src/resolvers/command.resolver';
|
|
6
|
+
import { OptionsResolver } from '../src/resolvers/options.resolver';
|
|
7
|
+
|
|
8
|
+
import SingleCommand from './commands/command.single';
|
|
9
|
+
import WithSubCommands from './commands/command.with-subcommands';
|
|
10
|
+
|
|
11
|
+
describe('CommandResolver', () => {
|
|
12
|
+
let commandResolver: CommandResolver;
|
|
13
|
+
let logger: ILogger;
|
|
14
|
+
let mockEnv: any;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
logger = { trace: jest.fn(), warn: jest.fn() } as any;
|
|
18
|
+
mockEnv = {
|
|
19
|
+
optional: jest.fn(),
|
|
20
|
+
required: jest.fn(),
|
|
21
|
+
isSet: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const optionsResolver = new OptionsResolver(mockEnv, logger);
|
|
25
|
+
|
|
26
|
+
commandResolver = new CommandResolver(optionsResolver, logger);
|
|
27
|
+
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
mockEnv.optional.mockClear();
|
|
33
|
+
mockEnv.required.mockClear();
|
|
34
|
+
mockEnv.isSet.mockClear();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('resolve', () => {
|
|
38
|
+
it('should create basic command without options', () => {
|
|
39
|
+
// Act
|
|
40
|
+
const result = commandResolver.resolve(SingleCommand);
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect(result).toBeInstanceOf(Command);
|
|
44
|
+
const metadata = MetadataRegistry.getCommand(SingleCommand);
|
|
45
|
+
|
|
46
|
+
expect(metadata).toBeDefined();
|
|
47
|
+
expect(result.name()).toBe(metadata.name);
|
|
48
|
+
expect(result.description()).toBe(metadata.description);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should create command with options', () => {
|
|
52
|
+
// Act
|
|
53
|
+
const result = commandResolver.resolve(SingleCommand);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(result).toBeInstanceOf(Command);
|
|
57
|
+
const options = MetadataRegistry.getOptions(SingleCommand);
|
|
58
|
+
|
|
59
|
+
expect(options).toBeDefined();
|
|
60
|
+
expect(result.options).toHaveLength(options.size);
|
|
61
|
+
|
|
62
|
+
// Verify each option matches the metadata
|
|
63
|
+
for (const [_, optionMeta] of options.entries()) {
|
|
64
|
+
const option = result.options.find((opt) =>
|
|
65
|
+
opt.flags.includes(`--${optionMeta.name}`),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(option).toBeDefined();
|
|
69
|
+
|
|
70
|
+
if (optionMeta.alias) {
|
|
71
|
+
expect(option!.flags).toContain(`-${optionMeta.alias}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should create command with subcommands', () => {
|
|
77
|
+
// Act
|
|
78
|
+
const metadata = MetadataRegistry.getCommand(WithSubCommands);
|
|
79
|
+
|
|
80
|
+
const command = commandResolver.resolve(
|
|
81
|
+
WithSubCommands,
|
|
82
|
+
metadata.subcommands,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
expect(command).toBeInstanceOf(Command);
|
|
87
|
+
expect(metadata.subcommands.length).toBe(2);
|
|
88
|
+
expect(command.commands).toHaveLength(metadata.subcommands.length);
|
|
89
|
+
|
|
90
|
+
// Verify each subcommand matches the metadata
|
|
91
|
+
for (const SubcommandClass of metadata.subcommands!) {
|
|
92
|
+
const subMeta = MetadataRegistry.getRunnable(SubcommandClass);
|
|
93
|
+
const subcommand = command.commands.find(
|
|
94
|
+
(cmd) => cmd.name() === subMeta.name,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(subcommand).toBeDefined();
|
|
98
|
+
expect(subcommand!.description()).toBe(subMeta.description);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should create command that executes with correct parameters', async () => {
|
|
103
|
+
// Arrange
|
|
104
|
+
const runMock = jest.fn();
|
|
105
|
+
const originalRun = SingleCommand.prototype.run;
|
|
106
|
+
|
|
107
|
+
SingleCommand.prototype.run = runMock;
|
|
108
|
+
|
|
109
|
+
mockEnv.optional
|
|
110
|
+
.mockReturnValueOnce({ key: 'MESSAGE', value: 'Test message' })
|
|
111
|
+
.mockReturnValueOnce({ key: 'VERBOSE', value: true });
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
const result = commandResolver.resolve(SingleCommand);
|
|
115
|
+
|
|
116
|
+
await result.parseAsync(['node', 'test']);
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
119
|
+
expect(runMock).toHaveBeenCalledWith('Test message', true);
|
|
120
|
+
|
|
121
|
+
// Cleanup
|
|
122
|
+
SingleCommand.prototype.run = originalRun;
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { LocalPathLoader, PkgLoader } from '../src/loaders/implementations';
|
|
4
|
+
|
|
5
|
+
import SingleCommand from './commands/command.single';
|
|
6
|
+
|
|
7
|
+
describe('Loaders', () => {
|
|
8
|
+
describe('LocalPathLoader', () => {
|
|
9
|
+
let loader: LocalPathLoader;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
loader = new LocalPathLoader();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should load commands from a JavaScript file', async () => {
|
|
16
|
+
const filePath = path.join(__dirname, './commands/js/command.single.js');
|
|
17
|
+
const command = await loader.load(filePath);
|
|
18
|
+
|
|
19
|
+
/* When loading from JS files, we can't use strict equality (.toBe)
|
|
20
|
+
* because the loaded function is a new instance.
|
|
21
|
+
* Instead, we verify the function name and prototype match
|
|
22
|
+
*/
|
|
23
|
+
expect(command.name).toBe(SingleCommand.name);
|
|
24
|
+
expect(command.prototype).toEqual(SingleCommand.prototype);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should load commands from a TypeScript file', async () => {
|
|
28
|
+
const filePath = path.join(__dirname, './commands/command.single.ts');
|
|
29
|
+
const command = await loader.load(filePath);
|
|
30
|
+
|
|
31
|
+
expect(command).toBe(SingleCommand);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw error when file not found', async () => {
|
|
35
|
+
const filePath = 'non-existent-file.ts';
|
|
36
|
+
|
|
37
|
+
await expect(loader.load(filePath)).rejects.toThrow(
|
|
38
|
+
'Error loading command from file',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('PkgLoader', () => {
|
|
44
|
+
let loader: PkgLoader;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
loader = new PkgLoader('@rsdk/cli');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should throw error when package not found', async () => {
|
|
51
|
+
await expect(loader.load('non-existent-command')).rejects.toThrow(
|
|
52
|
+
'Error loading package, try install it: yarn add -D @rsdk/cli.cmd.non-existent-command',
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* TODO: Как бы написать этот тест без моков
|
|
58
|
+
*/
|
|
59
|
+
xit('should load commands from a package', async () => {});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { OptionMetadata } from '@rsdk/cli.common';
|
|
2
|
+
import { NumberParser } from '@rsdk/common';
|
|
3
|
+
import type { Env } from '@rsdk/env';
|
|
4
|
+
import type { ILogger } from '@rsdk/logging';
|
|
5
|
+
|
|
6
|
+
import { OptionsResolver } from '../src/resolvers/options.resolver';
|
|
7
|
+
|
|
8
|
+
describe('OptionsResolver', () => {
|
|
9
|
+
let optionsResolver: OptionsResolver;
|
|
10
|
+
let mockLogger: jest.Mocked<ILogger>;
|
|
11
|
+
let mockEnv: jest.Mocked<Env>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* TODO: Заменить на noopLogger, когда он вольётся в develop
|
|
18
|
+
*/
|
|
19
|
+
mockLogger = {
|
|
20
|
+
warn: jest.fn(),
|
|
21
|
+
debug: jest.fn(),
|
|
22
|
+
error: jest.fn(),
|
|
23
|
+
info: jest.fn(),
|
|
24
|
+
trace: jest.fn(),
|
|
25
|
+
} as unknown as jest.Mocked<ILogger>;
|
|
26
|
+
|
|
27
|
+
mockEnv = {
|
|
28
|
+
optional: jest.fn(),
|
|
29
|
+
required: jest.fn(),
|
|
30
|
+
} as unknown as jest.Mocked<Env>;
|
|
31
|
+
|
|
32
|
+
optionsResolver = new OptionsResolver(mockEnv, mockLogger);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('resolve', () => {
|
|
36
|
+
it('should prioritize environment variable over flag and default value', () => {
|
|
37
|
+
const optionMetadata: OptionMetadata<number> = {
|
|
38
|
+
name: 'test-option',
|
|
39
|
+
description: 'Test option',
|
|
40
|
+
parser: new NumberParser(),
|
|
41
|
+
defaultValue: 1,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
mockEnv.optional.mockReturnValue({ value: 42, key: 'TEST_OPTION' });
|
|
45
|
+
|
|
46
|
+
const result = optionsResolver.resolve(optionMetadata, {
|
|
47
|
+
testOption: 10,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(42);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should use flag value when env variable is not set', () => {
|
|
54
|
+
const optionMetadata: OptionMetadata<number> = {
|
|
55
|
+
name: 'test-option',
|
|
56
|
+
description: 'Test option',
|
|
57
|
+
parser: new NumberParser(),
|
|
58
|
+
defaultValue: 1,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
62
|
+
mockEnv.optional.mockReturnValue(undefined);
|
|
63
|
+
|
|
64
|
+
const result = optionsResolver.resolve(optionMetadata, {
|
|
65
|
+
testOption: 10,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result).toBe(10);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should use default value when neither env nor flag is provided', () => {
|
|
72
|
+
const optionMetadata: OptionMetadata<number> = {
|
|
73
|
+
name: 'test-option',
|
|
74
|
+
description: 'Test option',
|
|
75
|
+
parser: new NumberParser(),
|
|
76
|
+
defaultValue: 1,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
80
|
+
mockEnv.optional.mockReturnValue(undefined);
|
|
81
|
+
|
|
82
|
+
const result = optionsResolver.resolve(optionMetadata, {});
|
|
83
|
+
|
|
84
|
+
expect(result).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle boolean flags without parser', () => {
|
|
88
|
+
const optionMetadata: OptionMetadata<boolean> = {
|
|
89
|
+
name: 'test-flag',
|
|
90
|
+
description: 'Test flag',
|
|
91
|
+
defaultValue: false,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
95
|
+
mockEnv.optional.mockReturnValue(undefined);
|
|
96
|
+
|
|
97
|
+
const result = optionsResolver.resolve(optionMetadata, {
|
|
98
|
+
testFlag: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle undefined default value', () => {
|
|
105
|
+
const optionMetadata: OptionMetadata<number> = {
|
|
106
|
+
name: 'test-option',
|
|
107
|
+
description: 'Test option',
|
|
108
|
+
parser: new NumberParser(),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
112
|
+
mockEnv.optional.mockReturnValue(undefined);
|
|
113
|
+
|
|
114
|
+
const result = optionsResolver.resolve(optionMetadata, {});
|
|
115
|
+
|
|
116
|
+
expect(result).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|