Skip to content

Commit f436299

Browse files
authored
feat(cli): add logger (#459)
1 parent 1858c81 commit f436299

File tree

16 files changed

+672
-70
lines changed

16 files changed

+672
-70
lines changed

package-lock.json

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

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"dependencies": {
4949
"@code-pushup/portal-client": "^0.4.1",
5050
"@isaacs/cliui": "^8.0.2",
51+
"@poppinss/cliui": "^6.3.0",
5152
"@swc/helpers": "0.5.3",
5253
"bundle-require": "^4.0.1",
5354
"chalk": "^5.3.0",

packages/cli/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@code-pushup/core": "*",
1111
"yargs": "^17.7.2",
1212
"chalk": "^5.3.0",
13-
"@code-pushup/utils": "*"
13+
"@code-pushup/utils": "*",
14+
"@poppinss/cliui": "^6.3.0"
1415
}
1516
}

packages/cli/src/lib/autorun/autorun-command.ts

+18-17
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import {
66
collectAndPersistReports,
77
upload,
88
} from '@code-pushup/core';
9+
import { getLatestCommit, validateCommitData } from '@code-pushup/utils';
910
import { CLI_NAME } from '../constants';
11+
import {
12+
collectSuccessfulLog,
13+
renderConfigureCategoriesHint,
14+
renderIntegratePortalHint,
15+
ui,
16+
uploadSuccessfulLog,
17+
} from '../implementation/logging';
1018
import { yargsOnlyPluginsOptionsDefinition } from '../implementation/only-plugins.options';
1119

1220
type AutorunOptions = CollectOptions & UploadOptions;
@@ -18,8 +26,8 @@ export function yargsAutorunCommandObject() {
1826
describe: 'Shortcut for running collect followed by upload',
1927
builder: yargsOnlyPluginsOptionsDefinition(),
2028
handler: async <T>(args: ArgumentsCamelCase<T>) => {
21-
console.info(chalk.bold(CLI_NAME));
22-
console.info(chalk.gray(`Run ${command}...`));
29+
ui().logger.log(chalk.bold(CLI_NAME));
30+
ui().logger.info(chalk.gray(`Run ${command}...`));
2331
const options = args as unknown as AutorunOptions;
2432

2533
// we need to ensure `json` is part of the formats as we want to upload
@@ -34,28 +42,21 @@ export function yargsAutorunCommandObject() {
3442
};
3543

3644
await collectAndPersistReports(optionsWithFormat);
45+
collectSuccessfulLog();
3746

3847
if (options.categories.length === 0) {
39-
console.info(
40-
chalk.gray(
41-
'💡 Configure categories to see the scores in an overview table. See: https://github.com/code-pushup/cli/blob/main/packages/cli/README.md',
42-
),
43-
);
48+
renderConfigureCategoriesHint();
4449
}
4550

4651
if (options.upload) {
4752
await upload(options);
53+
const commitData = await getLatestCommit();
54+
if (validateCommitData(commitData, { throwError: true })) {
55+
uploadSuccessfulLog(options.upload, commitData.hash);
56+
}
4857
} else {
49-
console.warn('Upload skipped because configuration is not set.');
50-
console.info(
51-
[
52-
'💡 Integrate the portal:',
53-
'- npx code-pushup upload - Run upload to upload the created report to the server',
54-
' https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command',
55-
'- Portal Integration - https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration',
56-
'- Upload Command - https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration',
57-
].join('\n'),
58-
);
58+
ui().logger.warning('Upload skipped because configuration is not set.');
59+
renderIntegratePortalHint();
5960
}
6061
},
6162
} satisfies CommandModule;

packages/cli/src/lib/collect/collect-command.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import {
44
CollectAndPersistReportsOptions,
55
collectAndPersistReports,
66
} from '@code-pushup/core';
7+
import { link } from '@code-pushup/utils';
78
import { CLI_NAME } from '../constants';
9+
import {
10+
collectSuccessfulLog,
11+
renderConfigureCategoriesHint,
12+
ui,
13+
} from '../implementation/logging';
814
import { yargsOnlyPluginsOptionsDefinition } from '../implementation/only-plugins.options';
915

1016
export function yargsCollectCommandObject(): CommandModule {
@@ -15,33 +21,51 @@ export function yargsCollectCommandObject(): CommandModule {
1521
builder: yargsOnlyPluginsOptionsDefinition(),
1622
handler: async <T>(args: ArgumentsCamelCase<T>) => {
1723
const options = args as unknown as CollectAndPersistReportsOptions;
18-
console.info(chalk.bold(CLI_NAME));
19-
console.info(chalk.gray(`Run ${command}...`));
24+
ui().logger.log(chalk.bold(CLI_NAME));
25+
ui().logger.info(chalk.gray(`Run ${command}...`));
26+
2027
await collectAndPersistReports(options);
28+
collectSuccessfulLog();
2129

2230
if (options.categories.length === 0) {
23-
console.info(
24-
chalk.gray(
25-
'💡 Configure categories to see the scores in an overview table. See: https://github.com/code-pushup/cli/blob/main/packages/cli/README.md',
26-
),
27-
);
31+
renderConfigureCategoriesHint();
2832
}
2933

3034
const { upload = {} } = args as unknown as Record<
3135
'upload',
3236
object | undefined
3337
>;
3438
if (Object.keys(upload).length === 0) {
35-
console.info(
36-
[
37-
'💡 Visualize your reports:',
38-
'- npx code-pushup upload - Run upload to upload the created report to the server',
39-
' https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command',
40-
'- npx code-pushup autorun - Run collect & upload',
41-
' https://github.com/code-pushup/cli/tree/main/packages/cli#autorun-command',
42-
].join('\n'),
43-
);
39+
renderUploadAutorunHint();
4440
}
4541
},
4642
} satisfies CommandModule;
4743
}
44+
45+
export function renderUploadAutorunHint(): void {
46+
ui()
47+
.sticker()
48+
.add(chalk.bold(chalk.gray('💡 Visualize your reports')))
49+
.add('')
50+
.add(
51+
`${chalk.gray('❯')} npx code-pushup upload - ${chalk.gray(
52+
'Run upload to upload the created report to the server',
53+
)}`,
54+
)
55+
.add(
56+
` ${link(
57+
'https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command',
58+
)}`,
59+
)
60+
.add(
61+
`${chalk.gray('❯')} npx code-pushup autorun - ${chalk.gray(
62+
'Run collect & upload',
63+
)}`,
64+
)
65+
.add(
66+
` ${link(
67+
'https://github.com/code-pushup/cli/tree/main/packages/cli#autorun-command',
68+
)}`,
69+
)
70+
.render();
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { cliui } from '@poppinss/cliui';
2+
import chalk from 'chalk';
3+
import { UploadConfig } from '@code-pushup/models';
4+
import { link, portalCommitDashboardLink } from '@code-pushup/utils';
5+
6+
export type CliUi = ReturnType<typeof cliui>;
7+
8+
// eslint-disable-next-line import/no-mutable-exports,functional/no-let
9+
export let singletonUiInstance: CliUi | undefined;
10+
export function ui(): CliUi {
11+
if (singletonUiInstance === undefined) {
12+
singletonUiInstance = cliui();
13+
}
14+
return singletonUiInstance;
15+
}
16+
17+
export function renderConfigureCategoriesHint(): void {
18+
ui().logger.info(
19+
chalk.gray(
20+
`💡 Configure categories to see the scores in an overview table. See: ${link(
21+
'https://github.com/code-pushup/cli/blob/main/packages/cli/README.md',
22+
)}`,
23+
),
24+
);
25+
}
26+
export function uploadSuccessfulLog(
27+
options: UploadConfig,
28+
commit: string,
29+
): void {
30+
ui().logger.success('Upload successful!');
31+
ui().logger.success(
32+
link(
33+
// @TODO extend config to maintain baseUrl under upload
34+
portalCommitDashboardLink(
35+
{ ...options, baseUrl: '<YOUR_PORTAL_URL>' },
36+
commit,
37+
),
38+
),
39+
);
40+
}
41+
export function collectSuccessfulLog(): void {
42+
ui().logger.success('Collecting report successful!');
43+
}
44+
45+
export function renderIntegratePortalHint(): void {
46+
ui()
47+
.sticker()
48+
.add(chalk.bold(chalk.gray('💡 Integrate the portal')))
49+
.add('')
50+
.add(
51+
`${chalk.gray('❯')} Upload a report to the server - ${chalk.gray(
52+
'npx code-pushup upload',
53+
)}`,
54+
)
55+
.add(
56+
` ${link(
57+
'https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command',
58+
)}`,
59+
)
60+
.add(
61+
`${chalk.gray('❯')} ${chalk.gray('Portal Integration')} - ${link(
62+
'https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration',
63+
)}`,
64+
)
65+
.add(
66+
`${chalk.gray('❯')} ${chalk.gray('Upload Command')} - ${link(
67+
'https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration',
68+
)}`,
69+
)
70+
.render();
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { expect } from 'vitest';
2+
import { ui } from './logging';
3+
4+
describe('ui', () => {
5+
it('should return singleton', () => {
6+
expect(ui()).toBe(ui());
7+
});
8+
});

packages/cli/src/lib/print-config/print-config-command.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CommandModule } from 'yargs';
22
import { filterKebabCaseKeys } from '../implementation/global.utils';
3+
import { ui } from '../implementation/logging';
34
import { onlyPluginsOption } from '../implementation/only-plugins.options';
45

56
export function yargsConfigCommandObject() {
@@ -16,7 +17,7 @@ export function yargsConfigCommandObject() {
1617
// it is important to filter out kebab case keys
1718
// because yargs duplicates options in camel case and kebab case
1819
const cleanArgs = filterKebabCaseKeys(args);
19-
console.info(JSON.stringify(cleanArgs, null, 2));
20+
ui().logger.log(JSON.stringify(cleanArgs, null, 2));
2021
},
2122
} satisfies CommandModule;
2223
}

packages/cli/src/lib/print-config/print-config-command.unit.test.ts

+28-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { describe, expect } from 'vitest';
1+
import { beforeAll, describe, expect } from 'vitest';
22
import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants';
3+
import { ui } from '../implementation/logging';
34
import { yargsCli } from '../yargs-cli';
45
import { yargsConfigCommandObject } from './print-config-command';
56

@@ -14,6 +15,11 @@ vi.mock('@code-pushup/core', async () => {
1415
});
1516

1617
describe('print-config-command', () => {
18+
beforeAll(() => {
19+
// initialize it in raw mode
20+
ui().switchMode('raw');
21+
});
22+
1723
it('should filter out meta arguments and kebab duplicates', async () => {
1824
await yargsCli(
1925
[
@@ -25,17 +31,30 @@ describe('print-config-command', () => {
2531
{ ...DEFAULT_CLI_CONFIGURATION, commands: [yargsConfigCommandObject()] },
2632
).parseAsync();
2733

28-
expect(console.info).not.toHaveBeenCalledWith(
29-
expect.stringContaining('"$0":'),
34+
const log = ui().logger.getLogs()[0];
35+
36+
expect(log).toEqual(
37+
expect.objectContaining({
38+
message: expect.not.stringContaining('"$0":'),
39+
}),
3040
);
31-
expect(console.info).not.toHaveBeenCalledWith(
32-
expect.stringContaining('"_":'),
41+
42+
expect(log).toEqual(
43+
expect.objectContaining({
44+
message: expect.not.stringContaining('"_":'),
45+
}),
3346
);
34-
expect(console.info).toHaveBeenCalledWith(
35-
expect.stringContaining('"outputDir": "destinationDir"'),
47+
48+
expect(log).toEqual(
49+
expect.objectContaining({
50+
message: expect.stringContaining('"outputDir": "destinationDir"'),
51+
}),
3652
);
37-
expect(console.info).not.toHaveBeenCalledWith(
38-
expect.stringContaining('"output-dir":'),
53+
54+
expect(log).toEqual(
55+
expect.objectContaining({
56+
message: expect.not.stringContaining('"output-dir":'),
57+
}),
3958
);
4059
});
4160
});
+14-11
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
11
import chalk from 'chalk';
22
import { ArgumentsCamelCase, CommandModule } from 'yargs';
33
import { UploadOptions, upload } from '@code-pushup/core';
4+
import { getLatestCommit, validateCommitData } from '@code-pushup/utils';
45
import { CLI_NAME } from '../constants';
6+
import {
7+
renderIntegratePortalHint,
8+
ui,
9+
uploadSuccessfulLog,
10+
} from '../implementation/logging';
511

612
export function yargsUploadCommandObject() {
713
const command = 'upload';
814
return {
915
command,
1016
describe: 'Upload report results to the portal',
1117
handler: async <T>(args: ArgumentsCamelCase<T>) => {
12-
console.info(chalk.bold(CLI_NAME));
13-
console.info(chalk.gray(`Run ${command}...`));
18+
ui().logger.log(chalk.bold(CLI_NAME));
19+
ui().logger.info(chalk.gray(`Run ${command}...`));
1420

1521
const options = args as unknown as UploadOptions;
1622
if (!options.upload) {
17-
console.info(
18-
[
19-
'💡 Integrate the portal:',
20-
'- npx code-pushup upload - Run upload to upload the created report to the server',
21-
' https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command',
22-
'- Portal Integration - https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration',
23-
'- Upload Command - https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration',
24-
].join('\n'),
25-
);
23+
renderIntegratePortalHint();
2624
throw new Error('Upload configuration not set');
2725
}
2826
await upload(options);
27+
28+
const commitData = await getLatestCommit();
29+
if (validateCommitData(commitData, { throwError: true })) {
30+
uploadSuccessfulLog(options.upload, commitData.hash);
31+
}
2932
},
3033
} satisfies CommandModule;
3134
}

packages/core/src/lib/implementation/persist.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
logMultipleFileResults,
1111
scoreReport,
1212
sortReport,
13+
validateCommitData,
1314
} from '@code-pushup/utils';
1415

1516
export class PersistDirError extends Error {
@@ -73,12 +74,6 @@ export async function persistReport(
7374
);
7475
}
7576

76-
function validateCommitData(commitData?: unknown) {
77-
if (!commitData) {
78-
console.warn('no commit data available');
79-
}
80-
}
81-
8277
async function persistResult(reportPath: string, content: string) {
8378
return (
8479
writeFile(reportPath, content)

0 commit comments

Comments
 (0)