Skip to content

Commit f3cebd3

Browse files
committed
feat(cli): log config import and validation steps
1 parent e0793ee commit f3cebd3

File tree

13 files changed

+116
-104
lines changed

13 files changed

+116
-104
lines changed

packages/cli/src/lib/implementation/core-config.middleware.int.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('coreConfigMiddleware', () => {
4040
it('should throw with invalid config path', async () => {
4141
await expect(
4242
coreConfigMiddleware({ config: 'wrong/path/to/config', ...CLI_DEFAULTS }),
43-
).rejects.toThrow(/Provided path .* is not valid./);
43+
).rejects.toThrow(/File '.*' does not exist/);
4444
});
4545

4646
it('should load config which relies on provided --tsconfig', async () => {

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

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
uploadConfigSchema,
1111
validate,
1212
} from '@code-pushup/models';
13+
import { logger, pluralizeToken } from '@code-pushup/utils';
1314
import type { CoreConfigCliOptions } from './core-config.model.js';
1415
import type { FilterOptions } from './filter.model.js';
1516
import type { GlobalOptions } from './global.model.js';
@@ -47,40 +48,45 @@ export async function coreConfigMiddleware<
4748
cache: cliCache,
4849
...remainingCliOptions
4950
} = processArgs;
50-
// Search for possible configuration file extensions if path is not given
51-
const importedRc = config
52-
? await readRcByPath(config, tsconfig)
53-
: await autoloadRc(tsconfig);
54-
const {
55-
persist: rcPersist,
56-
upload: rcUpload,
57-
...remainingRcConfig
58-
} = importedRc;
59-
const upload =
60-
rcUpload == null && cliUpload == null
61-
? undefined
62-
: validate(uploadConfigSchema, { ...rcUpload, ...cliUpload });
6351

64-
return {
65-
...(config != null && { config }),
66-
cache: normalizeCache(cliCache),
67-
persist: buildPersistConfig(cliPersist, rcPersist),
68-
...(upload != null && { upload }),
69-
...remainingRcConfig,
70-
...remainingCliOptions,
71-
};
52+
return logger.group('Loading configuration', async () => {
53+
// Search for possible configuration file extensions if path is not given
54+
const importedRc = config
55+
? await readRcByPath(config, tsconfig)
56+
: await autoloadRc(tsconfig);
57+
const {
58+
persist: rcPersist,
59+
upload: rcUpload,
60+
...remainingRcConfig
61+
} = importedRc;
62+
const upload =
63+
rcUpload == null && cliUpload == null
64+
? undefined
65+
: validate(uploadConfigSchema, { ...rcUpload, ...cliUpload });
66+
67+
const result: GlobalOptions & CoreConfig & FilterOptions = {
68+
...(config != null && { config }),
69+
cache: normalizeCache(cliCache),
70+
persist: buildPersistConfig(cliPersist, rcPersist),
71+
...(upload != null && { upload }),
72+
...remainingRcConfig,
73+
...remainingCliOptions,
74+
};
75+
76+
return {
77+
message: `Parsed config: ${summarizeConfig(result)}`,
78+
result,
79+
};
80+
});
7281
}
7382

74-
export const normalizeBooleanWithNegation = <T extends string>(
75-
propertyName: T,
76-
cliOptions?: Record<T, unknown>,
77-
rcOptions?: Record<T, unknown>,
78-
): boolean =>
79-
propertyName in (cliOptions ?? {})
80-
? (cliOptions?.[propertyName] as boolean)
81-
: `no-${propertyName}` in (cliOptions ?? {})
82-
? false
83-
: ((rcOptions?.[propertyName] as boolean) ?? true);
83+
function summarizeConfig(config: CoreConfig): string {
84+
return [
85+
pluralizeToken('plugin', config.plugins.length),
86+
pluralizeToken('category', config.categories?.length ?? 0),
87+
`upload ${config.upload ? 'enabled' : 'disabled'}`,
88+
].join(', ');
89+
}
8490

8591
export const normalizeCache = (cache?: CacheConfig): CacheConfigObject => {
8692
if (cache == null) {

packages/cli/src/lib/implementation/core-config.middleware.unit.test.ts

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { describe, expect, vi } from 'vitest';
22
import { autoloadRc, readRcByPath } from '@code-pushup/core';
33
import {
44
coreConfigMiddleware,
5-
normalizeBooleanWithNegation,
65
normalizeFormats,
76
} from './core-config.middleware.js';
87
import type { CoreConfigCliOptions } from './core-config.model.js';
98
import type { FilterOptions } from './filter.model.js';
10-
import type { GeneralCliOptions } from './global.model.js';
9+
import type { GlobalOptions } from './global.model.js';
1110

1211
vi.mock('@code-pushup/core', async () => {
1312
const { CORE_CONFIG_MOCK }: typeof import('@code-pushup/test-utils') =
@@ -20,36 +19,6 @@ vi.mock('@code-pushup/core', async () => {
2019
};
2120
});
2221

23-
describe('normalizeBooleanWithNegation', () => {
24-
it('should return true when CLI property is true', () => {
25-
expect(normalizeBooleanWithNegation('report', { report: true }, {})).toBe(
26-
true,
27-
);
28-
});
29-
30-
it('should return false when CLI property is false', () => {
31-
expect(normalizeBooleanWithNegation('report', { report: false }, {})).toBe(
32-
false,
33-
);
34-
});
35-
36-
it('should return false when no-property exists in CLI persist', () => {
37-
expect(
38-
normalizeBooleanWithNegation('report', { 'no-report': true }, {}),
39-
).toBe(false);
40-
});
41-
42-
it('should fallback to RC persist when no CLI property', () => {
43-
expect(normalizeBooleanWithNegation('report', {}, { report: false })).toBe(
44-
false,
45-
);
46-
});
47-
48-
it('should return default true when no property anywhere', () => {
49-
expect(normalizeBooleanWithNegation('report', {}, {})).toBe(true);
50-
});
51-
});
52-
5322
describe('normalizeFormats', () => {
5423
it('should forward valid formats', () => {
5524
expect(normalizeFormats(['json', 'md'])).toEqual(['json', 'md']);
@@ -71,15 +40,15 @@ describe('normalizeFormats', () => {
7140
describe('coreConfigMiddleware', () => {
7241
it('should attempt to load code-pushup.config.(ts|mjs|js) by default', async () => {
7342
await coreConfigMiddleware(
74-
{} as GeneralCliOptions & CoreConfigCliOptions & FilterOptions,
43+
{} as GlobalOptions & CoreConfigCliOptions & FilterOptions,
7544
);
7645
expect(autoloadRc).toHaveBeenCalled();
7746
});
7847

7948
it('should directly attempt to load passed config', async () => {
8049
await coreConfigMiddleware({
8150
config: 'cli/custom-config.mjs',
82-
} as GeneralCliOptions & CoreConfigCliOptions & FilterOptions);
51+
} as GlobalOptions & CoreConfigCliOptions & FilterOptions);
8352
expect(autoloadRc).not.toHaveBeenCalled();
8453
expect(readRcByPath).toHaveBeenCalledWith(
8554
'cli/custom-config.mjs',
@@ -90,15 +59,15 @@ describe('coreConfigMiddleware', () => {
9059
it('should forward --tsconfig option to config autoload', async () => {
9160
await coreConfigMiddleware({
9261
tsconfig: 'tsconfig.base.json',
93-
} as GeneralCliOptions & CoreConfigCliOptions & FilterOptions);
62+
} as GlobalOptions & CoreConfigCliOptions & FilterOptions);
9463
expect(autoloadRc).toHaveBeenCalledWith('tsconfig.base.json');
9564
});
9665

9766
it('should forward --tsconfig option to custom config load', async () => {
9867
await coreConfigMiddleware({
9968
config: 'apps/website/code-pushup.config.ts',
10069
tsconfig: 'apps/website/tsconfig.json',
101-
} as GeneralCliOptions & CoreConfigCliOptions & FilterOptions);
70+
} as GlobalOptions & CoreConfigCliOptions & FilterOptions);
10271
expect(readRcByPath).toHaveBeenCalledWith(
10372
'apps/website/code-pushup.config.ts',
10473
'apps/website/tsconfig.json',

packages/core/src/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ export {
2424
executePlugins,
2525
} from './lib/implementation/execute-plugin.js';
2626
export { persistReport } from './lib/implementation/persist.js';
27-
export {
28-
autoloadRc,
29-
ConfigPathError,
30-
readRcByPath,
31-
} from './lib/implementation/read-rc-file.js';
27+
export { autoloadRc, readRcByPath } from './lib/implementation/read-rc-file.js';
3228
export { AuditOutputsMissingAuditError } from './lib/implementation/runner.js';
3329
export { mergeDiffs } from './lib/merge-diffs.js';
3430
export { upload, type UploadOptions } from './lib/upload.js';

packages/core/src/lib/implementation/read-rc-file.int.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,13 @@ describe('readRcByPath', () => {
5555
});
5656

5757
it('should throw if the path is empty', async () => {
58-
await expect(readRcByPath('')).rejects.toThrow(
59-
'The path to the configuration file is empty.',
60-
);
58+
await expect(readRcByPath('')).rejects.toThrow("File '' does not exist");
6159
});
6260

6361
it('should throw if the file does not exist', async () => {
6462
await expect(
6563
readRcByPath(path.join('non-existent', 'config.file.js')),
66-
).rejects.toThrow(/Provided path .* is not valid./);
64+
).rejects.toThrow(/File '.*' does not exist/);
6765
});
6866

6967
it('should throw if the configuration is empty', async () => {

packages/core/src/lib/implementation/read-rc-file.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ansis from 'ansis';
12
import path from 'node:path';
23
import {
34
CONFIG_FILE_NAME,
@@ -6,36 +7,45 @@ import {
67
coreConfigSchema,
78
validate,
89
} from '@code-pushup/models';
9-
import { fileExists, importModule } from '@code-pushup/utils';
10-
11-
export class ConfigPathError extends Error {
12-
constructor(configPath: string) {
13-
super(`Provided path '${configPath}' is not valid.`);
14-
}
15-
}
10+
import { fileExists, importModule, logger } from '@code-pushup/utils';
1611

1712
export async function readRcByPath(
1813
filePath: string,
1914
tsconfig?: string,
2015
): Promise<CoreConfig> {
21-
if (filePath.length === 0) {
22-
throw new Error('The path to the configuration file is empty.');
23-
}
24-
25-
if (!(await fileExists(filePath))) {
26-
throw new ConfigPathError(filePath);
27-
}
16+
const formattedTarget = [
17+
`${ansis.bold(path.relative(process.cwd(), filePath))}`,
18+
tsconfig &&
19+
`(paths from ${ansis.bold(path.relative(process.cwd(), tsconfig))})`,
20+
]
21+
.filter(Boolean)
22+
.join(' ');
2823

29-
const cfg: CoreConfig = await importModule({
30-
filepath: filePath,
31-
tsconfig,
32-
format: 'esm',
33-
});
24+
const value = await logger.task(
25+
`Importing config from ${formattedTarget}`,
26+
async () => {
27+
const result = await importModule({
28+
filepath: filePath,
29+
tsconfig,
30+
format: 'esm',
31+
});
32+
return { result, message: `Imported config from ${formattedTarget}` };
33+
},
34+
);
3435

35-
return validate(coreConfigSchema, cfg, { filePath });
36+
const config = validate(coreConfigSchema, value, { filePath });
37+
logger.info('Configuration is valid ✓');
38+
return config;
3639
}
3740

3841
export async function autoloadRc(tsconfig?: string): Promise<CoreConfig> {
42+
const configFilePatterns = [
43+
CONFIG_FILE_NAME,
44+
`{${SUPPORTED_CONFIG_FILE_FORMATS.join(',')}}`,
45+
].join('.');
46+
47+
logger.debug(`Looking for default config file ${configFilePatterns}`);
48+
3949
// eslint-disable-next-line functional/no-let
4050
let ext = '';
4151
// eslint-disable-next-line functional/no-loop-statements
@@ -44,16 +54,15 @@ export async function autoloadRc(tsconfig?: string): Promise<CoreConfig> {
4454
const exists = await fileExists(filePath);
4555

4656
if (exists) {
57+
logger.debug(`Found default config file ${ansis.bold(filePath)}`);
4758
ext = extension;
4859
break;
4960
}
5061
}
5162

5263
if (!ext) {
5364
throw new Error(
54-
`No file ${CONFIG_FILE_NAME}.(${SUPPORTED_CONFIG_FILE_FORMATS.join(
55-
'|',
56-
)}) present in ${process.cwd()}`,
65+
`No ${configFilePatterns} file present in ${process.cwd()}`,
5766
);
5867
}
5968

packages/core/src/lib/implementation/read-rc-file.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('autoloadRc', () => {
7676

7777
it('should throw if no configuration file is present', async () => {
7878
await expect(autoloadRc()).rejects.toThrow(
79-
'No file code-pushup.config.(ts|mjs|js) present in',
79+
`No code-pushup.config.{ts,mjs,js} file present in ${MEMFS_VOLUME}`,
8080
);
8181
});
8282
});

packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,9 @@ describe('getCoveragePathForJest', () => {
261261
vol.fromJSON(
262262
{
263263
// values come from bundle-require mock above
264+
'jest-preset.config.ts': '',
264265
'jest-valid.config.unit.ts': '',
266+
'jest-valid.config.integration.ts': '',
265267
'jest-no-dir.config.integration.ts': '',
266268
'jest-no-lcov.config.integration.ts': '',
267269
},

packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ describe('getConfig', () => {
368368
});
369369

370370
it('should load config from lh-config.js file if configPath is specified', async () => {
371+
vol.fromJSON({ 'lh-config.js': '// mocked above' }, MEMFS_VOLUME);
371372
await expect(getConfig({ configPath: 'lh-config.js' })).resolves.toEqual(
372373
expect.objectContaining({
373374
upload: expect.objectContaining({
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "key": "value" }

0 commit comments

Comments
 (0)