@poetora/cli 0.0.1 → 0.1.2
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +93 -0
- package/bin/accessibility.js +2 -2
- package/bin/cli-builder.d.ts +8 -0
- package/bin/cli-builder.js +178 -0
- package/bin/cli.d.ts +5 -11
- package/bin/cli.js +8 -200
- package/bin/commands/base.command.d.ts +13 -0
- package/bin/commands/base.command.js +40 -0
- package/bin/commands/check.command.d.ts +14 -0
- package/bin/commands/check.command.js +21 -0
- package/bin/commands/dev.command.d.ts +13 -0
- package/bin/commands/dev.command.js +40 -0
- package/bin/commands/index.d.ts +6 -0
- package/bin/commands/index.js +6 -0
- package/bin/commands/init.command.d.ts +16 -0
- package/bin/commands/init.command.js +88 -0
- package/bin/commands/link.command.d.ts +13 -0
- package/bin/commands/link.command.js +19 -0
- package/bin/commands/update.command.d.ts +10 -0
- package/bin/commands/update.command.js +13 -0
- package/bin/constants.js +1 -1
- package/bin/errors/cli-error.d.ts +26 -0
- package/bin/errors/cli-error.js +53 -0
- package/bin/errors/index.d.ts +1 -0
- package/bin/errors/index.js +1 -0
- package/bin/index.d.ts +1 -1
- package/bin/index.js +6 -6
- package/bin/mdxAccessibility.js +2 -2
- package/bin/services/accessibility-check.service.d.ts +10 -0
- package/bin/services/accessibility-check.service.js +144 -0
- package/bin/services/index.d.ts +7 -0
- package/bin/services/index.js +7 -0
- package/bin/services/link.service.d.ts +7 -0
- package/bin/services/link.service.js +40 -0
- package/bin/services/openapi-check.service.d.ts +7 -0
- package/bin/services/openapi-check.service.js +43 -0
- package/bin/services/port.service.d.ts +7 -0
- package/bin/services/port.service.js +26 -0
- package/bin/services/template.service.d.ts +22 -0
- package/bin/services/template.service.js +127 -0
- package/bin/services/update.service.d.ts +10 -0
- package/bin/services/update.service.js +57 -0
- package/bin/services/version.service.d.ts +16 -0
- package/bin/services/version.service.js +102 -0
- package/bin/types/common.d.ts +38 -0
- package/bin/types/common.js +21 -0
- package/bin/types/index.d.ts +2 -0
- package/bin/types/index.js +2 -0
- package/bin/types/options.d.ts +23 -0
- package/bin/types/options.js +1 -0
- package/bin/utils/console-logger.d.ts +16 -0
- package/bin/utils/console-logger.js +65 -0
- package/bin/utils/index.d.ts +2 -0
- package/bin/utils/index.js +2 -0
- package/bin/utils/logger.interface.d.ts +15 -0
- package/bin/utils/logger.interface.js +1 -0
- package/package.json +29 -29
- package/src/accessibility.ts +2 -2
- package/src/cli-builder.ts +267 -0
- package/src/cli.ts +15 -0
- package/src/commands/__tests__/base.command.test.ts +145 -0
- package/src/commands/__tests__/dev.command.test.ts +241 -0
- package/src/commands/__tests__/init.command.test.ts +281 -0
- package/{__test__ → src/commands/__tests__}/utils.ts +1 -1
- package/src/commands/base.command.ts +97 -0
- package/src/commands/check.command.ts +40 -0
- package/src/commands/dev.command.ts +63 -0
- package/src/commands/index.ts +6 -0
- package/src/commands/init.command.ts +125 -0
- package/src/commands/link.command.ts +39 -0
- package/src/commands/update.command.ts +23 -0
- package/src/constants.ts +1 -1
- package/src/errors/cli-error.ts +83 -0
- package/src/errors/index.ts +1 -0
- package/src/index.ts +6 -6
- package/src/mdxAccessibility.ts +3 -4
- package/src/services/__tests__/port.service.test.ts +83 -0
- package/src/services/__tests__/template.service.test.ts +234 -0
- package/src/services/__tests__/version.service.test.ts +165 -0
- package/src/services/accessibility-check.service.ts +226 -0
- package/src/services/index.ts +7 -0
- package/src/services/link.service.ts +65 -0
- package/src/services/openapi-check.service.ts +68 -0
- package/src/services/port.service.ts +47 -0
- package/src/services/template.service.ts +203 -0
- package/src/services/update.service.ts +76 -0
- package/src/services/version.service.ts +161 -0
- package/src/types/common.ts +53 -0
- package/src/types/index.ts +2 -0
- package/src/types/options.ts +42 -0
- package/src/utils/console-logger.ts +114 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/logger.interface.ts +70 -0
- package/tsconfig.build.json +2 -1
- package/tsconfig.json +1 -1
- package/.prettierignore +0 -2
- package/__test__/brokenLinks.test.ts +0 -93
- package/__test__/checkPort.test.ts +0 -92
- package/__test__/openApiCheck.test.ts +0 -127
- package/__test__/update.test.ts +0 -108
- package/bin/accessibilityCheck.d.ts +0 -2
- package/bin/accessibilityCheck.js +0 -70
- package/bin/helpers.d.ts +0 -17
- package/bin/helpers.js +0 -104
- package/bin/init.d.ts +0 -1
- package/bin/init.js +0 -73
- package/bin/mdxLinter.d.ts +0 -2
- package/bin/mdxLinter.js +0 -45
- package/bin/update.d.ts +0 -3
- package/bin/update.js +0 -32
- package/src/accessibilityCheck.tsx +0 -145
- package/src/cli.tsx +0 -302
- package/src/helpers.tsx +0 -131
- package/src/init.tsx +0 -93
- package/src/mdxLinter.tsx +0 -88
- package/src/update.tsx +0 -37
package/src/accessibility.ts
CHANGED
|
@@ -157,8 +157,8 @@ export function checkDocsColors(
|
|
|
157
157
|
...anchorResults.flatMap((anchor) => [anchor.lightContrast, anchor.darkContrast]),
|
|
158
158
|
].filter(Boolean);
|
|
159
159
|
|
|
160
|
-
const hasFailure = results.some((result) => result
|
|
161
|
-
const hasWarning = results.some((result) => result
|
|
160
|
+
const hasFailure = results.some((result) => result?.recommendation === 'fail');
|
|
161
|
+
const hasWarning = results.some((result) => result?.recommendation === 'warning');
|
|
162
162
|
|
|
163
163
|
let overallScore: 'pass' | 'warning' | 'fail';
|
|
164
164
|
if (hasFailure) {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { Argv } from 'yargs';
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CheckCommand,
|
|
7
|
+
DevCommand,
|
|
8
|
+
InitCommand,
|
|
9
|
+
LinkCommand,
|
|
10
|
+
UpdateCommand,
|
|
11
|
+
} from './commands/index.js';
|
|
12
|
+
import {
|
|
13
|
+
AccessibilityCheckService,
|
|
14
|
+
LinkService,
|
|
15
|
+
OpenApiCheckService,
|
|
16
|
+
PortService,
|
|
17
|
+
TemplateService,
|
|
18
|
+
UpdateService,
|
|
19
|
+
VersionService,
|
|
20
|
+
} from './services/index.js';
|
|
21
|
+
import type { DevOptions, OpenApiCheckOptions, RenameOptions } from './types/index.js';
|
|
22
|
+
import type { ILogger } from './utils/index.js';
|
|
23
|
+
import { ConsoleLogger } from './utils/index.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CLI Builder - Orchestrates all commands using yargs
|
|
27
|
+
*/
|
|
28
|
+
export class CliBuilder {
|
|
29
|
+
private logger: ILogger;
|
|
30
|
+
|
|
31
|
+
constructor(private readonly packageName: string = 'poet') {
|
|
32
|
+
this.logger = new ConsoleLogger();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build and configure the CLI
|
|
37
|
+
*/
|
|
38
|
+
build(): Argv {
|
|
39
|
+
// Create services
|
|
40
|
+
const versionService = new VersionService(this.packageName);
|
|
41
|
+
const portService = new PortService(this.logger);
|
|
42
|
+
const templateService = new TemplateService();
|
|
43
|
+
const openApiCheckService = new OpenApiCheckService(this.logger);
|
|
44
|
+
const accessibilityCheckService = new AccessibilityCheckService();
|
|
45
|
+
const linkService = new LinkService(this.logger);
|
|
46
|
+
const updateService = new UpdateService(this.logger, versionService, this.packageName);
|
|
47
|
+
|
|
48
|
+
// Create commands
|
|
49
|
+
const devCommand = new DevCommand(this.logger, versionService, portService, this.packageName);
|
|
50
|
+
const initCommand = new InitCommand(this.logger, templateService, this.packageName);
|
|
51
|
+
const checkCommand = new CheckCommand(
|
|
52
|
+
this.logger,
|
|
53
|
+
openApiCheckService,
|
|
54
|
+
accessibilityCheckService,
|
|
55
|
+
this.packageName
|
|
56
|
+
);
|
|
57
|
+
const linkCommand = new LinkCommand(this.logger, linkService, this.packageName);
|
|
58
|
+
const updateCommand = new UpdateCommand(this.logger, updateService, this.packageName);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
yargs(hideBin(process.argv))
|
|
62
|
+
.scriptName(this.packageName)
|
|
63
|
+
// Dev command
|
|
64
|
+
.command(
|
|
65
|
+
'dev',
|
|
66
|
+
'initialize a local preview environment',
|
|
67
|
+
(yargs) =>
|
|
68
|
+
yargs
|
|
69
|
+
.option('port', {
|
|
70
|
+
type: 'number',
|
|
71
|
+
description: 'port to run the preview server on',
|
|
72
|
+
default: 3000,
|
|
73
|
+
})
|
|
74
|
+
.option('open', {
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
default: true,
|
|
77
|
+
description: 'open a local preview in the browser',
|
|
78
|
+
})
|
|
79
|
+
.option('local-schema', {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
default: false,
|
|
82
|
+
hidden: true,
|
|
83
|
+
description:
|
|
84
|
+
'use a locally hosted schema file (note: only https protocol is supported in production)',
|
|
85
|
+
})
|
|
86
|
+
.option('client-version', {
|
|
87
|
+
type: 'string',
|
|
88
|
+
hidden: true,
|
|
89
|
+
description: 'the version of the client to use for cli testing',
|
|
90
|
+
})
|
|
91
|
+
.option('groups', {
|
|
92
|
+
type: 'array',
|
|
93
|
+
description: 'Mock user groups for local development and testing',
|
|
94
|
+
example: '--groups admin user',
|
|
95
|
+
})
|
|
96
|
+
.option('disable-openapi', {
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
default: false,
|
|
99
|
+
description: 'Disable OpenAPI file generation',
|
|
100
|
+
}),
|
|
101
|
+
async (argv) => {
|
|
102
|
+
try {
|
|
103
|
+
await devCommand.run(argv as DevOptions);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
// Init/New command
|
|
111
|
+
.command(
|
|
112
|
+
['new [directory]', 'init [directory]'],
|
|
113
|
+
'Create a new Poetora documentation site',
|
|
114
|
+
(yargs) =>
|
|
115
|
+
yargs.positional('directory', {
|
|
116
|
+
describe: 'The directory to initialize your documentation',
|
|
117
|
+
type: 'string',
|
|
118
|
+
default: '.',
|
|
119
|
+
}),
|
|
120
|
+
async (argv) => {
|
|
121
|
+
try {
|
|
122
|
+
await initCommand.run({ directory: argv.directory as string });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
// OpenAPI check command
|
|
130
|
+
.command(
|
|
131
|
+
'openapi-check <filename>',
|
|
132
|
+
'check if an OpenAPI spec is valid',
|
|
133
|
+
(yargs) =>
|
|
134
|
+
yargs
|
|
135
|
+
.positional('filename', {
|
|
136
|
+
describe:
|
|
137
|
+
'the filename of the OpenAPI spec (e.g. ./openapi.yaml) or the URL to the OpenAPI spec',
|
|
138
|
+
type: 'string',
|
|
139
|
+
demandOption: true,
|
|
140
|
+
})
|
|
141
|
+
.option('local-schema', {
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
default: false,
|
|
144
|
+
description:
|
|
145
|
+
'use a locally hosted schema file (note: only https protocol is supported in production)',
|
|
146
|
+
}),
|
|
147
|
+
async (argv) => {
|
|
148
|
+
try {
|
|
149
|
+
await checkCommand.checkOpenApi({
|
|
150
|
+
filename: argv.filename as string,
|
|
151
|
+
localSchema: argv.localSchema,
|
|
152
|
+
} as OpenApiCheckOptions);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
// Accessibility check command
|
|
161
|
+
.command(
|
|
162
|
+
['a11y', 'accessibility-check', 'a11y-check', 'accessibility'],
|
|
163
|
+
'check for accessibility issues in documentation',
|
|
164
|
+
() => undefined,
|
|
165
|
+
async () => {
|
|
166
|
+
try {
|
|
167
|
+
const exitCode = await checkCommand.checkAccessibility();
|
|
168
|
+
process.exit(exitCode);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
// Broken links command
|
|
176
|
+
.command(
|
|
177
|
+
'broken-links',
|
|
178
|
+
'check for invalid internal links',
|
|
179
|
+
() => undefined,
|
|
180
|
+
async () => {
|
|
181
|
+
try {
|
|
182
|
+
const brokenLinks = await linkCommand.checkBrokenLinks();
|
|
183
|
+
const hasBrokenLinks = Object.keys(brokenLinks).length > 0;
|
|
184
|
+
process.exit(hasBrokenLinks ? 1 : 0);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
// Rename command
|
|
192
|
+
.command(
|
|
193
|
+
'rename <from> <to>',
|
|
194
|
+
'rename a file and update all internal link references',
|
|
195
|
+
(yargs) =>
|
|
196
|
+
yargs
|
|
197
|
+
.positional('from', {
|
|
198
|
+
describe: 'the file to rename',
|
|
199
|
+
type: 'string',
|
|
200
|
+
})
|
|
201
|
+
.positional('to', {
|
|
202
|
+
describe: 'the new name for the file',
|
|
203
|
+
type: 'string',
|
|
204
|
+
})
|
|
205
|
+
.demandOption(['from', 'to'])
|
|
206
|
+
.option('force', {
|
|
207
|
+
type: 'boolean',
|
|
208
|
+
default: false,
|
|
209
|
+
description: 'rename files and skip errors',
|
|
210
|
+
})
|
|
211
|
+
.epilog(`example: \`${this.packageName} rename introduction.mdx overview.mdx\``),
|
|
212
|
+
async (argv) => {
|
|
213
|
+
try {
|
|
214
|
+
await linkCommand.renameFile({
|
|
215
|
+
from: argv.from as string,
|
|
216
|
+
to: argv.to as string,
|
|
217
|
+
force: argv.force,
|
|
218
|
+
} as RenameOptions);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
// Update command
|
|
227
|
+
.command(
|
|
228
|
+
'update',
|
|
229
|
+
'update the CLI to the latest version',
|
|
230
|
+
() => undefined,
|
|
231
|
+
async () => {
|
|
232
|
+
try {
|
|
233
|
+
await updateCommand.run({});
|
|
234
|
+
process.exit(0);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
this.logger.error(error instanceof Error ? error.message : 'Unknown error');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
// Version command
|
|
242
|
+
.command(
|
|
243
|
+
['version', 'v'],
|
|
244
|
+
'display the current version of the CLI and client',
|
|
245
|
+
() => undefined,
|
|
246
|
+
async () => {
|
|
247
|
+
const versions = versionService.getVersions();
|
|
248
|
+
this.logger.log(`cli version: ${versions.cli}`);
|
|
249
|
+
this.logger.log(`client version: ${versions.client}`);
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
// Error handling
|
|
254
|
+
.strictCommands()
|
|
255
|
+
.demandCommand(1, 'unknown command. see above for the list of supported commands.')
|
|
256
|
+
.alias('h', 'help')
|
|
257
|
+
.alias('v', 'version')
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Parse and execute CLI
|
|
263
|
+
*/
|
|
264
|
+
async run(): Promise<void> {
|
|
265
|
+
await this.build().parseAsync();
|
|
266
|
+
}
|
|
267
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CliBuilder } from './cli-builder.js';
|
|
3
|
+
|
|
4
|
+
export interface CliOptions {
|
|
5
|
+
packageName: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const cli = (options: CliOptions): void => {
|
|
9
|
+
const builder = new CliBuilder(options.packageName);
|
|
10
|
+
|
|
11
|
+
builder.run().catch((error) => {
|
|
12
|
+
console.error('Fatal error:', error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { CliError, ValidationError } from '../../errors/index.js';
|
|
3
|
+
import type { ILogger } from '../../utils/index.js';
|
|
4
|
+
import { BaseCommand } from '../base.command.js';
|
|
5
|
+
|
|
6
|
+
// Test command implementation
|
|
7
|
+
class TestCommand extends BaseCommand {
|
|
8
|
+
readonly name = 'test';
|
|
9
|
+
readonly description = 'Test command';
|
|
10
|
+
|
|
11
|
+
public validateCalled = false;
|
|
12
|
+
public executeCalled = false;
|
|
13
|
+
|
|
14
|
+
protected override async validate(options: { value: string }): Promise<void> {
|
|
15
|
+
this.validateCalled = true;
|
|
16
|
+
if (!options.value) {
|
|
17
|
+
throw new ValidationError('value is required');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected override async execute(options: { value: string }): Promise<string> {
|
|
22
|
+
this.executeCalled = true;
|
|
23
|
+
return `executed with ${options.value}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('BaseCommand', () => {
|
|
28
|
+
let mockLogger: ILogger;
|
|
29
|
+
let command: TestCommand;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
mockLogger = {
|
|
33
|
+
info: vi.fn(),
|
|
34
|
+
success: vi.fn(),
|
|
35
|
+
error: vi.fn(),
|
|
36
|
+
warn: vi.fn(),
|
|
37
|
+
log: vi.fn(),
|
|
38
|
+
spinner: vi.fn(),
|
|
39
|
+
logColor: vi.fn(),
|
|
40
|
+
logBold: vi.fn(),
|
|
41
|
+
logSeparator: vi.fn(),
|
|
42
|
+
logNewLine: vi.fn(),
|
|
43
|
+
logHeader: vi.fn(),
|
|
44
|
+
};
|
|
45
|
+
command = new TestCommand(mockLogger);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('run', () => {
|
|
49
|
+
it('should execute command successfully', async () => {
|
|
50
|
+
const result = await command.run({ value: 'test' });
|
|
51
|
+
|
|
52
|
+
expect(command.validateCalled).toBe(true);
|
|
53
|
+
expect(command.executeCalled).toBe(true);
|
|
54
|
+
expect(result).toBe('executed with test');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should validate before execute', async () => {
|
|
58
|
+
const callOrder: string[] = [];
|
|
59
|
+
|
|
60
|
+
// Use type assertion to bypass protected access for testing
|
|
61
|
+
(command as unknown as { validate: typeof command.validate }).validate = vi.fn(async () => {
|
|
62
|
+
callOrder.push('validate');
|
|
63
|
+
});
|
|
64
|
+
(command as unknown as { execute: typeof command.execute }).execute = vi.fn(async () => {
|
|
65
|
+
callOrder.push('execute');
|
|
66
|
+
return 'result';
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await command.run({ value: 'test' });
|
|
70
|
+
|
|
71
|
+
expect(callOrder).toEqual(['validate', 'execute']);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle validation errors', async () => {
|
|
75
|
+
await expect(command.run({ value: '' })).rejects.toThrow(ValidationError);
|
|
76
|
+
|
|
77
|
+
expect(command.validateCalled).toBe(true);
|
|
78
|
+
expect(command.executeCalled).toBe(false);
|
|
79
|
+
expect(mockLogger.error).toHaveBeenCalledWith('value is required');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle execution errors', async () => {
|
|
83
|
+
(command as unknown as { execute: typeof command.execute }).execute = vi
|
|
84
|
+
.fn()
|
|
85
|
+
.mockRejectedValue(new Error('execution failed'));
|
|
86
|
+
|
|
87
|
+
await expect(command.run({ value: 'test' })).rejects.toThrow('execution failed');
|
|
88
|
+
|
|
89
|
+
expect(mockLogger.error).toHaveBeenCalledWith('execution failed');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle CliError specially', async () => {
|
|
93
|
+
const cliError = new CliError('CLI error message', 'TEST_ERROR', 1);
|
|
94
|
+
(command as unknown as { execute: typeof command.execute }).execute = vi
|
|
95
|
+
.fn()
|
|
96
|
+
.mockRejectedValue(cliError);
|
|
97
|
+
|
|
98
|
+
await expect(command.run({ value: 'test' })).rejects.toThrow(cliError);
|
|
99
|
+
|
|
100
|
+
expect(mockLogger.error).toHaveBeenCalledWith('CLI error message');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle unknown errors', async () => {
|
|
104
|
+
(command as unknown as { execute: typeof command.execute }).execute = vi
|
|
105
|
+
.fn()
|
|
106
|
+
.mockRejectedValue('string error');
|
|
107
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
108
|
+
|
|
109
|
+
await expect(command.run({ value: 'test' })).rejects.toBe('string error');
|
|
110
|
+
|
|
111
|
+
expect(mockLogger.error).toHaveBeenCalledWith('An unexpected error occurred');
|
|
112
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('string error');
|
|
113
|
+
|
|
114
|
+
consoleErrorSpy.mockRestore();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should show stack trace in debug mode', async () => {
|
|
118
|
+
const originalDebug = process.env.DEBUG;
|
|
119
|
+
process.env.DEBUG = 'true';
|
|
120
|
+
|
|
121
|
+
const error = new Error('test error');
|
|
122
|
+
(command as unknown as { execute: typeof command.execute }).execute = vi
|
|
123
|
+
.fn()
|
|
124
|
+
.mockRejectedValue(error);
|
|
125
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
126
|
+
|
|
127
|
+
await expect(command.run({ value: 'test' })).rejects.toThrow(error);
|
|
128
|
+
|
|
129
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(error.stack);
|
|
130
|
+
|
|
131
|
+
consoleErrorSpy.mockRestore();
|
|
132
|
+
process.env.DEBUG = originalDebug;
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('command metadata', () => {
|
|
137
|
+
it('should have name property', () => {
|
|
138
|
+
expect(command.name).toBe('test');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should have description property', () => {
|
|
142
|
+
expect(command.description).toBe('Test command');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import * as previewing from '@poetora/previewing';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { InvalidEnvironmentError } from '../../errors/index.js';
|
|
4
|
+
import type { PortService, VersionService } from '../../services/index.js';
|
|
5
|
+
import type { DevOptions } from '../../types/index.js';
|
|
6
|
+
import type { ILogger } from '../../utils/index.js';
|
|
7
|
+
import { DevCommand } from '../dev.command.js';
|
|
8
|
+
|
|
9
|
+
vi.mock('@poetora/previewing', async () => {
|
|
10
|
+
const actual = await vi.importActual<typeof import('@poetora/previewing')>('@poetora/previewing');
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
dev: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('DevCommand', () => {
|
|
18
|
+
let mockLogger: ILogger;
|
|
19
|
+
let mockVersionService: VersionService;
|
|
20
|
+
let mockPortService: PortService;
|
|
21
|
+
let command: DevCommand;
|
|
22
|
+
const devSpy = vi.mocked(previewing.dev);
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockLogger = {
|
|
26
|
+
info: vi.fn(),
|
|
27
|
+
success: vi.fn(),
|
|
28
|
+
error: vi.fn(),
|
|
29
|
+
warn: vi.fn(),
|
|
30
|
+
log: vi.fn(),
|
|
31
|
+
spinner: vi.fn(),
|
|
32
|
+
logColor: vi.fn(),
|
|
33
|
+
logBold: vi.fn(),
|
|
34
|
+
logSeparator: vi.fn(),
|
|
35
|
+
logNewLine: vi.fn(),
|
|
36
|
+
logHeader: vi.fn(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
mockVersionService = {
|
|
40
|
+
checkNodeVersion: vi.fn(),
|
|
41
|
+
getCliVersion: vi.fn(),
|
|
42
|
+
parseNodeVersion: vi.fn(),
|
|
43
|
+
getClientVersion: vi.fn(),
|
|
44
|
+
getVersions: vi.fn(),
|
|
45
|
+
getLatestCliVersion: vi.fn(),
|
|
46
|
+
isVersionUpToDate: vi.fn(),
|
|
47
|
+
validateNodeVersion: vi.fn(),
|
|
48
|
+
} as unknown as VersionService;
|
|
49
|
+
|
|
50
|
+
mockPortService = {
|
|
51
|
+
findAvailablePort: vi.fn(),
|
|
52
|
+
} as unknown as PortService;
|
|
53
|
+
|
|
54
|
+
command = new DevCommand(mockLogger, mockVersionService, mockPortService, 'poet');
|
|
55
|
+
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('validate', () => {
|
|
60
|
+
it('should pass validation for supported Node version', async () => {
|
|
61
|
+
vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
|
|
62
|
+
isValid: true,
|
|
63
|
+
hasWarning: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await expect(command.validate({} as DevOptions)).resolves.not.toThrow();
|
|
67
|
+
|
|
68
|
+
expect(mockVersionService.checkNodeVersion).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should show warning for below recommended version', async () => {
|
|
72
|
+
vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
|
|
73
|
+
isValid: true,
|
|
74
|
+
hasWarning: true,
|
|
75
|
+
message: 'Node.js 20.17.0 recommended',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await command.validate({} as DevOptions);
|
|
79
|
+
|
|
80
|
+
expect(mockLogger.warn).toHaveBeenCalledWith('Node.js 20.17.0 recommended');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error for unsupported Node version', async () => {
|
|
84
|
+
vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
|
|
85
|
+
isValid: false,
|
|
86
|
+
hasWarning: false,
|
|
87
|
+
message: 'Node.js 18.0.0 or higher required',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await expect(command.validate({} as DevOptions)).rejects.toThrow(InvalidEnvironmentError);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('execute', () => {
|
|
95
|
+
it('should start dev server with default options', async () => {
|
|
96
|
+
vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3000);
|
|
97
|
+
vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
|
|
98
|
+
devSpy.mockResolvedValue(undefined);
|
|
99
|
+
|
|
100
|
+
const options: DevOptions = {};
|
|
101
|
+
|
|
102
|
+
await command.execute(options);
|
|
103
|
+
|
|
104
|
+
expect(mockPortService.findAvailablePort).toHaveBeenCalledWith(undefined);
|
|
105
|
+
expect(mockVersionService.getCliVersion).toHaveBeenCalled();
|
|
106
|
+
expect(devSpy).toHaveBeenCalledWith({
|
|
107
|
+
_: [],
|
|
108
|
+
$0: 'poet',
|
|
109
|
+
port: 3000,
|
|
110
|
+
open: true,
|
|
111
|
+
localSchema: false,
|
|
112
|
+
clientVersion: undefined,
|
|
113
|
+
groups: undefined,
|
|
114
|
+
disableOpenapi: false,
|
|
115
|
+
packageName: 'poet',
|
|
116
|
+
cliVersion: '1.0.0',
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should use specified port', async () => {
|
|
121
|
+
vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(5000);
|
|
122
|
+
vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
|
|
123
|
+
devSpy.mockResolvedValue(undefined);
|
|
124
|
+
|
|
125
|
+
const options: DevOptions = { port: 5000 };
|
|
126
|
+
|
|
127
|
+
await command.execute(options);
|
|
128
|
+
|
|
129
|
+
expect(mockPortService.findAvailablePort).toHaveBeenCalledWith(5000);
|
|
130
|
+
expect(devSpy).toHaveBeenCalledWith(
|
|
131
|
+
expect.objectContaining({
|
|
132
|
+
port: 5000,
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should respect open option', async () => {
|
|
138
|
+
vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3000);
|
|
139
|
+
vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
|
|
140
|
+
devSpy.mockResolvedValue(undefined);
|
|
141
|
+
|
|
142
|
+
const options: DevOptions = { open: false };
|
|
143
|
+
|
|
144
|
+
await command.execute(options);
|
|
145
|
+
|
|
146
|
+
expect(devSpy).toHaveBeenCalledWith(
|
|
147
|
+
expect.objectContaining({
|
|
148
|
+
open: false,
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should pass all options correctly', async () => {
|
|
154
|
+
vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(4000);
|
|
155
|
+
vi.mocked(mockVersionService.getCliVersion).mockReturnValue('2.0.0');
|
|
156
|
+
devSpy.mockResolvedValue(undefined);
|
|
157
|
+
|
|
158
|
+
const options: DevOptions = {
|
|
159
|
+
port: 4000,
|
|
160
|
+
open: false,
|
|
161
|
+
localSchema: true,
|
|
162
|
+
clientVersion: '1.5.0',
|
|
163
|
+
groups: ['admin', 'user'],
|
|
164
|
+
disableOpenapi: true,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
await command.execute(options);
|
|
168
|
+
|
|
169
|
+
expect(devSpy).toHaveBeenCalledWith({
|
|
170
|
+
_: [],
|
|
171
|
+
$0: 'poet',
|
|
172
|
+
port: 4000,
|
|
173
|
+
open: false,
|
|
174
|
+
localSchema: true,
|
|
175
|
+
clientVersion: '1.5.0',
|
|
176
|
+
groups: ['admin', 'user'],
|
|
177
|
+
disableOpenapi: true,
|
|
178
|
+
packageName: 'poet',
|
|
179
|
+
cliVersion: '2.0.0',
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should handle when port service finds alternative port', async () => {
|
|
184
|
+
vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3001);
|
|
185
|
+
vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
|
|
186
|
+
devSpy.mockResolvedValue(undefined);
|
|
187
|
+
|
|
188
|
+
const options: DevOptions = { port: 3000 };
|
|
189
|
+
|
|
190
|
+
await command.execute(options);
|
|
191
|
+
|
|
192
|
+
expect(devSpy).toHaveBeenCalledWith(
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
port: 3001, // Alternative port found
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('run (integration)', () => {
|
|
201
|
+
it('should execute full flow successfully', async () => {
|
|
202
|
+
vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
|
|
203
|
+
isValid: true,
|
|
204
|
+
hasWarning: false,
|
|
205
|
+
});
|
|
206
|
+
vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3000);
|
|
207
|
+
vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
|
|
208
|
+
devSpy.mockResolvedValue(undefined);
|
|
209
|
+
|
|
210
|
+
await command.run({ port: 3000 });
|
|
211
|
+
|
|
212
|
+
expect(mockVersionService.checkNodeVersion).toHaveBeenCalled();
|
|
213
|
+
expect(mockPortService.findAvailablePort).toHaveBeenCalled();
|
|
214
|
+
expect(devSpy).toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should fail at validation stage for unsupported Node version', async () => {
|
|
218
|
+
vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
|
|
219
|
+
isValid: false,
|
|
220
|
+
hasWarning: false,
|
|
221
|
+
message: 'Unsupported Node.js version',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await expect(command.run({})).rejects.toThrow(InvalidEnvironmentError);
|
|
225
|
+
|
|
226
|
+
// Should not reach execute stage
|
|
227
|
+
expect(mockPortService.findAvailablePort).not.toHaveBeenCalled();
|
|
228
|
+
expect(devSpy).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('metadata', () => {
|
|
233
|
+
it('should have correct name', () => {
|
|
234
|
+
expect(command.name).toBe('dev');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should have correct description', () => {
|
|
238
|
+
expect(command.description).toBe('initialize a local preview environment');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|