Skip to content

Commit ba048be

Browse files
feat(cli): initial collect command (#45)
Co-authored-by: Michael <[email protected]> Co-authored-by: Michael Hladky <[email protected]>
1 parent 37ea0a5 commit ba048be

28 files changed

+531
-332
lines changed

package-lock.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"chalk": "^5.3.0",
1010
"yargs": "^17.7.2",
1111
"zod": "^3.22.1",
12-
"@quality-metrics/models": "^0.0.1"
12+
"@quality-metrics/models": "^0.0.1",
13+
"@quality-metrics/utils": "^0.0.1"
1314
}
1415
}

packages/cli/src/lib/cli.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { join } from 'path';
12
import { describe, expect, it } from 'vitest';
23
import { yargsCli } from './cli';
3-
import { join } from 'path';
4-
import { yargsGlobalOptionsDefinition } from './options';
5-
import { middlewares } from './middlewares';
64
import { CommandBase } from './implementation/base-command-config';
75
import { getDirname } from './implementation/utils';
6+
import { middlewares } from './middlewares';
7+
import { yargsGlobalOptionsDefinition } from './options';
88

99
const __dirname = getDirname(import.meta.url);
1010
const withDirName = (path: string) => join(__dirname, path);

packages/cli/src/lib/cli.ts

+22-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import { CoreConfig } from '@quality-metrics/models';
2+
import chalk from 'chalk';
13
import yargs, {
24
Argv,
35
CommandModule,
46
MiddlewareFunction,
57
Options,
68
ParserConfigurationOptions,
79
} from 'yargs';
8-
import chalk from 'chalk';
9-
import { CoreConfig } from '@quality-metrics/models';
10+
import { logErrorBeforeThrow } from './implementation/utils';
1011

1112
/**
1213
* returns configurable yargs CLI for code-pushup
@@ -40,6 +41,8 @@ export function yargsCli(
4041

4142
// setup yargs
4243
cli
44+
.help()
45+
.alias('h', 'help')
4346
.parserConfiguration({
4447
'strip-dashed': true,
4548
} satisfies Partial<ParserConfigurationOptions>)
@@ -57,12 +60,25 @@ export function yargsCli(
5760
}
5861

5962
// add middlewares
60-
middlewares.forEach(({ middlewareFunction, applyBeforeValidation }) =>
61-
cli.middleware(middlewareFunction, applyBeforeValidation),
62-
);
63+
middlewares.forEach(({ middlewareFunction, applyBeforeValidation }) => {
64+
cli.middleware(
65+
logErrorBeforeThrow(middlewareFunction),
66+
applyBeforeValidation,
67+
);
68+
});
6369

6470
// add commands
65-
commands.forEach(commandObj => cli.command(commandObj));
71+
commands.forEach(commandObj => {
72+
cli.command({
73+
...commandObj,
74+
...(commandObj.handler && {
75+
handler: logErrorBeforeThrow(commandObj.handler),
76+
}),
77+
...(typeof commandObj.builder === 'function' && {
78+
builder: logErrorBeforeThrow(commandObj.builder),
79+
}),
80+
});
81+
});
6682

6783
// return CLI object
6884
return cli as unknown as Argv<CoreConfig>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const { runnerOutputSchema } = require('@quality-metrics/models');
2+
module.exports = {
3+
persist: { outputPath: 'command-object-config-out.json' },
4+
plugins: [
5+
{
6+
audits: [
7+
{
8+
slug: 'command-object-audit-slug',
9+
title: 'audit title',
10+
description: 'audit description',
11+
label: 'mock audit label',
12+
docsUrl: 'http://www.my-docs.dev',
13+
},
14+
],
15+
runner: {
16+
command: 'bash',
17+
args: [
18+
'-c',
19+
`echo '${JSON.stringify(
20+
runnerOutputSchema.parse({
21+
audits: [
22+
{
23+
slug: 'command-object-audit-slug',
24+
value: 0,
25+
score: 0,
26+
},
27+
],
28+
}),
29+
)}' > command-object-config-out.json`,
30+
],
31+
outputPath: 'command-object-config-out.json',
32+
},
33+
groups: [],
34+
meta: {
35+
slug: 'command-object-plugin',
36+
name: 'command-object plugin',
37+
},
38+
},
39+
],
40+
categories: [],
41+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { CoreConfig, PluginConfig, Report } from '@quality-metrics/models';
2+
import { CollectOptions } from '@quality-metrics/utils';
3+
import { readFileSync } from 'node:fs';
4+
import { join } from 'node:path';
5+
import { yargsCli } from '../cli';
6+
import { getDirname } from '../implementation/utils';
7+
import { middlewares } from '../middlewares';
8+
import { yargsGlobalOptionsDefinition } from '../options';
9+
import { yargsCollectCommandObject } from './command-object';
10+
11+
const outputPath = 'collect-command-object.json';
12+
const dummyConfig: CoreConfig = {
13+
persist: { outputPath },
14+
plugins: [mockPlugin()],
15+
categories: [],
16+
};
17+
18+
describe('collect-command-object', () => {
19+
it('should parse arguments correctly', async () => {
20+
const args = ['collect', '--verbose', '--configPath', ''];
21+
const cli = yargsCli([], { options: yargsGlobalOptionsDefinition() })
22+
.config(dummyConfig)
23+
.command(yargsCollectCommandObject());
24+
const parsedArgv = (await cli.parseAsync(
25+
args,
26+
)) as unknown as CollectOptions;
27+
const { persist } = parsedArgv;
28+
const { outputPath: outPath } = persist;
29+
expect(outPath).toBe(outputPath);
30+
return Promise.resolve(void 0);
31+
});
32+
33+
it('should execute middleware correctly', async () => {
34+
const args = [
35+
'collect',
36+
'--configPath',
37+
join(
38+
getDirname(import.meta.url),
39+
'..',
40+
'implementation',
41+
'mock',
42+
'config-middleware-config.mock.mjs',
43+
),
44+
];
45+
await yargsCli([], { middlewares })
46+
.config(dummyConfig)
47+
.command(yargsCollectCommandObject())
48+
.parseAsync(args);
49+
const report = JSON.parse(readFileSync(outputPath).toString()) as Report;
50+
expect(report.plugins[0]?.meta.slug).toBe('collect-command-object');
51+
expect(report.plugins[0]?.audits[0]?.slug).toBe(
52+
'command-object-audit-slug',
53+
);
54+
});
55+
});
56+
57+
function mockPlugin(): PluginConfig {
58+
return {
59+
audits: [
60+
{
61+
slug: 'command-object-audit-slug',
62+
title: 'audit title',
63+
description: 'audit description',
64+
label: 'mock audit label',
65+
docsUrl: 'http://www.my-docs.dev',
66+
},
67+
],
68+
runner: {
69+
command: 'bash',
70+
args: [
71+
'-c',
72+
`echo '${JSON.stringify({
73+
audits: [
74+
{
75+
slug: 'command-object-audit-slug',
76+
value: 0,
77+
score: 0,
78+
},
79+
],
80+
})}' > ${outputPath}`,
81+
],
82+
outputPath,
83+
},
84+
groups: [],
85+
meta: {
86+
slug: 'collect-command-object',
87+
name: 'collect command object',
88+
},
89+
} satisfies PluginConfig;
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { collect, CollectOptions } from '@quality-metrics/utils';
2+
import { writeFile } from 'fs/promises';
3+
import { CommandModule } from 'yargs';
4+
5+
export function yargsCollectCommandObject() {
6+
const handler = async (args: CollectOptions): Promise<void> => {
7+
const collectOutput = await collect(args);
8+
9+
const { persist } = args;
10+
await writeFile(persist.outputPath, JSON.stringify(collectOutput, null, 2));
11+
};
12+
13+
return {
14+
command: 'collect',
15+
describe: 'Run Plugins and collect results',
16+
handler: handler as unknown as CommandModule['handler'],
17+
} satisfies CommandModule;
18+
}

packages/cli/src/lib/commands.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import { CommandModule } from 'yargs';
2+
import { yargsCollectCommandObject } from './collect/command-object';
23

3-
export const commands: CommandModule[] = [];
4+
export const commands: CommandModule[] = [yargsCollectCommandObject()];

packages/cli/src/lib/implementation/config-middleware.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { join } from 'path';
2-
import { configMiddleware, ConfigParseError } from './config-middleware';
32
import { expect } from 'vitest';
3+
import { configMiddleware, ConfigParseError } from './config-middleware';
44
import { getDirname } from './utils';
55

66
const __dirname = getDirname(import.meta.url);

packages/cli/src/lib/implementation/config-middleware.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { existsSync } from 'node:fs';
21
import { bundleRequire } from 'bundle-require';
2+
import { stat } from 'fs/promises';
33

44
import { GlobalCliArgs, globalCliArgsSchema } from '@quality-metrics/models';
55
import { CommandBase, commandBaseSchema } from './base-command-config';
@@ -15,7 +15,12 @@ export async function configMiddleware<T = unknown>(
1515
): Promise<CommandBase> {
1616
const globalCfg: GlobalCliArgs = globalCliArgsSchema.parse(processArgs);
1717
const { configPath } = globalCfg;
18-
if (!existsSync(configPath)) {
18+
try {
19+
const stats = await stat(configPath);
20+
if (!stats.isFile) {
21+
throw new ConfigParseError(configPath);
22+
}
23+
} catch (err) {
1924
throw new ConfigParseError(configPath);
2025
}
2126

packages/cli/src/lib/implementation/utils.ts

+18
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,21 @@ import { fileURLToPath } from 'url';
33

44
export const getDirname = (import_meta_url: string) =>
55
dirname(fileURLToPath(import_meta_url));
6+
7+
// log error and flush stdout so that Yargs doesn't supress it
8+
// related issue: https://github.com/yargs/yargs/issues/2118
9+
export function logErrorBeforeThrow<
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
T extends (...args: any[]) => any,
12+
>(fn: T): T {
13+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14+
return (async (...args: any[]) => {
15+
try {
16+
return await fn(...args);
17+
} catch (err) {
18+
console.error(err);
19+
await new Promise(resolve => process.stdout.write('', resolve));
20+
throw err;
21+
}
22+
}) as T;
23+
}

packages/cli/src/lib/middlewares.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { configMiddleware } from './implementation/config-middleware';
21
import { MiddlewareFunction } from 'yargs';
2+
import { configMiddleware } from './implementation/config-middleware';
33

44
export const middlewares: {
55
middlewareFunction: MiddlewareFunction;

packages/models/src/index.ts

+17-10
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
1+
export { CategoryConfig, categoryConfigSchema } from './lib/category-config';
12
export {
2-
unrefinedCoreConfigSchema,
3-
refineCoreConfig,
4-
coreConfigSchema,
53
CoreConfig,
4+
coreConfigSchema,
5+
refineCoreConfig,
6+
unrefinedCoreConfigSchema,
67
} from './lib/core-config';
7-
export { uploadConfigSchema, UploadConfig } from './lib/upload-config';
8+
export { GlobalCliArgs, globalCliArgsSchema } from './lib/global-cli-options';
9+
export { PersistConfig, persistConfigSchema } from './lib/persist-config';
810
export {
9-
pluginConfigSchema,
11+
AuditGroup,
12+
AuditMetadata,
13+
Issue,
1014
PluginConfig,
1115
RunnerOutput,
16+
auditGroupSchema,
17+
auditMetadataSchema,
18+
issueSchema,
19+
pluginConfigSchema,
1220
runnerOutputSchema,
1321
} from './lib/plugin-config';
1422
export {
15-
runnerOutputAuditRefsPresentInPluginConfigs,
16-
reportSchema,
23+
PluginOutput,
24+
PluginReport,
1725
Report,
26+
runnerOutputAuditRefsPresentInPluginConfigs,
1827
} from './lib/report';
19-
export { persistConfigSchema, PersistConfig } from './lib/persist-config';
20-
export { categoryConfigSchema, CategoryConfig } from './lib/category-config';
21-
export { globalCliArgsSchema, GlobalCliArgs } from './lib/global-cli-options';
28+
export { UploadConfig, uploadConfigSchema } from './lib/upload-config';

0 commit comments

Comments
 (0)