Skip to content

Commit 3b5b977

Browse files
committed
feat(nx-plugin): add general executor logic
1 parent 2f8d78e commit 3b5b977

28 files changed

+1129
-91
lines changed

packages/core/src/lib/upload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type UploadOptions = { upload?: UploadConfig } & {
1414
/**
1515
* Uploads collected audits to the portal
1616
* @param options
17+
* @param uploadFn
1718
*/
1819
export async function upload(
1920
options: UploadOptions,

packages/nx-plugin/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
#### Init
66

7-
Install JS packages and register plugin
7+
Install JS packages and register plugin.
8+
See [init docs](./src/generators/init/Readme.md) for details
89

910
Examples:
1011

@@ -13,7 +14,8 @@ Examples:
1314

1415
#### Configuration
1516

16-
Adds a `code-pushup` target to your `project.json`
17+
Adds a `code-pushup` target to your `project.json`.
18+
See [configuration docs](./src/generators/configuration/Readme.md) for details
1719

1820
Examples:
1921

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const ENV = {
2+
CP_SERVER: 'https://portal.code.pushup.dev',
3+
CP_ORGANIZATION: 'code-pushup',
4+
CP_PROJECT: 'utils',
5+
CP_API_KEY: '23456789098765432345678909876543',
6+
CP_TIMEOUT: 9,
7+
};

packages/nx-plugin/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
"dependencies": {
66
"@nx/devkit": "^17.1.3",
77
"tslib": "2.6.2",
8-
"nx": "^17.1.3"
8+
"nx": "^17.1.3",
9+
"@code-pushup/models": "*",
10+
"@code-pushup/utils": "*",
11+
"zod": "^3.22.4"
912
},
1013
"type": "commonjs",
1114
"main": "./src/index.js",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { CliArgsObject } from '@code-pushup/utils';
2+
3+
export function createCliCommand(
4+
command: string,
5+
args: Record<string, unknown>,
6+
): string {
7+
return `npx @code-pushup/cli ${command} ${objectToCliArgs(args).join(' ')}`;
8+
}
9+
10+
type ArgumentValue = number | string | boolean | string[];
11+
// @TODO import from @code-pushup/utils => get rid of poppins for cjs support
12+
// eslint-disable-next-line sonarjs/cognitive-complexity
13+
export function objectToCliArgs<
14+
T extends object = Record<string, ArgumentValue>,
15+
>(params?: CliArgsObject<T>): string[] {
16+
if (!params) {
17+
return [];
18+
}
19+
20+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
21+
return Object.entries(params).flatMap(([key, value]) => {
22+
// process/file/script
23+
if (key === '_') {
24+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
25+
return Array.isArray(value) ? value : [`${value}`];
26+
}
27+
const prefix = key.length === 1 ? '-' : '--';
28+
// "-*" arguments (shorthands)
29+
if (Array.isArray(value)) {
30+
return value.map(v => `${prefix}${key}="${v}"`);
31+
}
32+
// "--*" arguments ==========
33+
34+
if (Array.isArray(value)) {
35+
return value.map(v => `${prefix}${key}="${v}"`);
36+
}
37+
38+
if (typeof value === 'object') {
39+
return Object.entries(value as Record<string, unknown>).map(
40+
([k, v]) => `${prefix}${key}.${k}="${v?.toString()}"`,
41+
);
42+
}
43+
44+
if (typeof value === 'string') {
45+
return [`${prefix}${key}="${value}"`];
46+
}
47+
48+
if (typeof value === 'number') {
49+
return [`${prefix}${key}=${value}`];
50+
}
51+
52+
if (typeof value === 'boolean') {
53+
return [`${prefix}${value ? '' : 'no-'}${key}`];
54+
}
55+
56+
if (value === undefined) {
57+
return [];
58+
}
59+
60+
throw new Error(`Unsupported type ${typeof value} for key ${key}`);
61+
});
62+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { objectToCliArgs } from './cli';
3+
4+
describe('objectToCliArgs', () => {
5+
it('should handle the "_" argument as script', () => {
6+
const params = { _: 'bin.js' };
7+
const result = objectToCliArgs(params);
8+
expect(result).toEqual(['bin.js']);
9+
});
10+
11+
it('should handle the "_" argument with multiple values', () => {
12+
const params = { _: ['bin.js', '--help'] };
13+
const result = objectToCliArgs(params);
14+
expect(result).toEqual(['bin.js', '--help']);
15+
});
16+
17+
it('should handle shorthands arguments', () => {
18+
const params = {
19+
e: `test`,
20+
};
21+
const result = objectToCliArgs(params);
22+
expect(result).toEqual([`-e="${params.e}"`]);
23+
});
24+
25+
it('should handle string arguments', () => {
26+
const params = { name: 'Juanita' };
27+
const result = objectToCliArgs(params);
28+
expect(result).toEqual(['--name="Juanita"']);
29+
});
30+
31+
it('should handle number arguments', () => {
32+
const params = { parallel: 5 };
33+
const result = objectToCliArgs(params);
34+
expect(result).toEqual(['--parallel=5']);
35+
});
36+
37+
it('should handle boolean arguments', () => {
38+
const params = { progress: true };
39+
const result = objectToCliArgs(params);
40+
expect(result).toEqual(['--progress']);
41+
});
42+
43+
it('should handle negated boolean arguments', () => {
44+
const params = { progress: false };
45+
const result = objectToCliArgs(params);
46+
expect(result).toEqual(['--no-progress']);
47+
});
48+
49+
it('should handle array of string arguments', () => {
50+
const params = { format: ['json', 'md'] };
51+
const result = objectToCliArgs(params);
52+
expect(result).toEqual(['--format="json"', '--format="md"']);
53+
});
54+
55+
it('should handle objects', () => {
56+
const params = { format: { json: 'simple' } };
57+
const result = objectToCliArgs(params);
58+
expect(result).toStrictEqual(['--format.json="simple"']);
59+
});
60+
61+
it('should throw error for unsupported type', () => {
62+
expect(() => objectToCliArgs({ param: Symbol('') })).toThrow(
63+
'Unsupported type',
64+
);
65+
});
66+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect } from 'vitest';
2+
import { ENV } from '../../../mock/fixtures/env';
3+
import { uploadConfig } from './config';
4+
import { parseEnv } from './env';
5+
6+
vi.mock('./env', async () => {
7+
const actual = await vi.importActual('./env');
8+
return {
9+
...actual,
10+
parseEnv: vi.fn(actual.parseEnv as typeof parseEnv),
11+
};
12+
});
13+
14+
describe('uploadConfig', () => {
15+
it('should call parseEnv function with values from process.env', () => {
16+
const old = process.env;
17+
18+
// eslint-disable-next-line functional/immutable-data
19+
process.env = ENV;
20+
21+
expect(
22+
uploadConfig(
23+
{
24+
server: 'https://portal.code.pushup.dev',
25+
},
26+
{
27+
workspaceRoot: 'workspaceRoot',
28+
projectConfig: {
29+
name: 'my-app',
30+
root: 'root',
31+
},
32+
},
33+
),
34+
).toEqual(
35+
expect.objectContaining({
36+
server: ENV.CP_SERVER,
37+
apiKey: ENV.CP_API_KEY,
38+
organization: ENV.CP_ORGANIZATION,
39+
project: ENV.CP_PROJECT,
40+
}),
41+
);
42+
43+
expect(parseEnv).toHaveBeenCalledTimes(1);
44+
expect(parseEnv).toHaveBeenCalledWith(ENV);
45+
46+
// eslint-disable-next-line functional/immutable-data
47+
process.env = old;
48+
});
49+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { join } from 'node:path';
2+
import type { PersistConfig, UploadConfig } from '@code-pushup/models';
3+
import { slugify } from '../../internal/utils';
4+
import { parseEnv } from './env';
5+
import {
6+
BaseNormalizedExecutorContext,
7+
GlobalExecutorOptions,
8+
ProjectExecutorOnlyOptions,
9+
} from './types';
10+
11+
export function globalConfig(
12+
options: Partial<GlobalExecutorOptions>,
13+
context: BaseNormalizedExecutorContext,
14+
): Required<GlobalExecutorOptions> {
15+
const { projectConfig } = context;
16+
const { root: projectRoot = '' } = projectConfig ?? {};
17+
// For better debugging use `--verbose --no-progress` as default
18+
const { verbose, progress, config } = options;
19+
return {
20+
verbose: !!verbose,
21+
progress: !!progress,
22+
config: config ?? join(projectRoot, 'code-pushup.config.json'),
23+
};
24+
}
25+
26+
export function persistConfig(
27+
options: Partial<PersistConfig & ProjectExecutorOnlyOptions>,
28+
context: BaseNormalizedExecutorContext,
29+
): Partial<PersistConfig> {
30+
const { projectConfig } = context;
31+
32+
const { name: projectName = '', root: projectRoot = '' } =
33+
projectConfig ?? {};
34+
const {
35+
format = ['json'],
36+
outputDir = join(projectRoot, '.code-pushup', projectName), // always in <root>/.code-pushup/<project-name>,
37+
filename: filenameOptions,
38+
} = options;
39+
40+
return {
41+
format,
42+
outputDir,
43+
...(filenameOptions ? { filename: slugify(filenameOptions) } : {}),
44+
};
45+
}
46+
47+
export function uploadConfig(
48+
options: Partial<UploadConfig & ProjectExecutorOnlyOptions>,
49+
context: BaseNormalizedExecutorContext,
50+
): Partial<UploadConfig> {
51+
const { projectConfig, workspaceRoot } = context;
52+
53+
const { name: projectName } = projectConfig ?? {};
54+
const { projectPrefix, server, apiKey, organization, project, timeout } =
55+
options;
56+
const applyPrefix = workspaceRoot === '.';
57+
const prefix = projectPrefix ? `${projectPrefix}-` : '';
58+
return {
59+
...(projectName
60+
? {
61+
project: applyPrefix ? `${prefix}${projectName}` : projectName, // provide correct project
62+
}
63+
: {}),
64+
...parseEnv(process.env),
65+
...Object.fromEntries(
66+
Object.entries({ server, apiKey, organization, project, timeout }).filter(
67+
([_, v]) => v !== undefined,
68+
),
69+
),
70+
};
71+
}

0 commit comments

Comments
 (0)