Skip to content

Commit ff74241

Browse files
committed
refactor: adjust tests
1 parent a2892ba commit ff74241

File tree

10 files changed

+248
-339
lines changed

10 files changed

+248
-339
lines changed

packages/nx-plugin/src/executors/internal/cli.unit.test.ts

Lines changed: 1 addition & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import {
3-
createCliCommandObject,
4-
createCliCommandString,
5-
objectToCliArgs,
6-
} from './cli.js';
7-
8-
describe('objectToCliArgs', () => {
9-
it('should empty params', () => {
10-
const result = objectToCliArgs();
11-
expect(result).toEqual([]);
12-
});
13-
14-
it('should handle the "_" argument as script', () => {
15-
const params = { _: 'bin.js' };
16-
const result = objectToCliArgs(params);
17-
expect(result).toEqual(['bin.js']);
18-
});
19-
20-
it('should handle the "_" argument with multiple values', () => {
21-
const params = { _: ['bin.js', '--help'] };
22-
const result = objectToCliArgs(params);
23-
expect(result).toEqual(['bin.js', '--help']);
24-
});
25-
26-
it('should handle shorthands arguments', () => {
27-
const params = {
28-
e: `test`,
29-
};
30-
const result = objectToCliArgs(params);
31-
expect(result).toEqual([`-e="${params.e}"`]);
32-
});
33-
34-
it('should handle string arguments', () => {
35-
const params = { name: 'Juanita' };
36-
const result = objectToCliArgs(params);
37-
expect(result).toEqual(['--name="Juanita"']);
38-
});
39-
40-
it('should handle number arguments', () => {
41-
const params = { parallel: 5 };
42-
const result = objectToCliArgs(params);
43-
expect(result).toEqual(['--parallel=5']);
44-
});
45-
46-
it('should handle boolean arguments', () => {
47-
const params = { progress: true };
48-
const result = objectToCliArgs(params);
49-
expect(result).toEqual(['--progress']);
50-
});
51-
52-
it('should handle negated boolean arguments', () => {
53-
const params = { progress: false };
54-
const result = objectToCliArgs(params);
55-
expect(result).toEqual(['--no-progress']);
56-
});
57-
58-
it('should handle array of string arguments', () => {
59-
const params = { format: ['json', 'md'] };
60-
const result = objectToCliArgs(params);
61-
expect(result).toEqual(['--format="json"', '--format="md"']);
62-
});
63-
64-
it('should handle objects', () => {
65-
const params = { format: { json: 'simple' } };
66-
const result = objectToCliArgs(params);
67-
expect(result).toStrictEqual(['--format.json="simple"']);
68-
});
69-
70-
it('should handle nested objects', () => {
71-
const params = { persist: { format: ['json', 'md'], verbose: false } };
72-
const result = objectToCliArgs(params);
73-
expect(result).toEqual([
74-
'--persist.format="json"',
75-
'--persist.format="md"',
76-
'--no-persist.verbose',
77-
]);
78-
});
79-
80-
it('should handle objects with undefined', () => {
81-
const params = { format: undefined };
82-
const result = objectToCliArgs(params);
83-
expect(result).toStrictEqual([]);
84-
});
85-
86-
it('should throw error for unsupported type', () => {
87-
expect(() => objectToCliArgs({ param: Symbol('') })).toThrow(
88-
'Unsupported type',
89-
);
90-
});
91-
});
92-
93-
describe('createCliCommandString', () => {
94-
it('should create command out of object for arguments', () => {
95-
const result = createCliCommandString({ args: { verbose: true } });
96-
expect(result).toBe('npx @code-pushup/cli --verbose');
97-
});
98-
99-
it('should create command out of object for arguments with positional', () => {
100-
const result = createCliCommandString({
101-
args: { _: 'autorun', verbose: true },
102-
});
103-
expect(result).toBe('npx @code-pushup/cli autorun --verbose');
104-
});
105-
});
2+
import { createCliCommandObject } from './cli.js';
1063

1074
describe('createCliCommandObject', () => {
1085
it('should create command out of object for arguments', () => {

packages/nx-plugin/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const plugin = {
1111
export default plugin;
1212

1313
export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js';
14-
export { objectToCliArgs } from './executors/internal/cli.js';
14+
export { objectToCliArgs } from './internal/command.js';
1515
export { generateCodePushupConfig } from './generators/configuration/code-pushup-config.js';
1616
export { configurationGenerator } from './generators/configuration/generator.js';
1717
export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js';
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import ansis from 'ansis';
2+
import path from 'node:path';
3+
4+
type ArgumentValue = number | string | boolean | string[] | undefined;
5+
export type CliArgsObject<T extends object = Record<string, ArgumentValue>> =
6+
T extends never
7+
? Record<string, ArgumentValue | undefined> | { _: string }
8+
: T;
9+
10+
/**
11+
* Escapes command line arguments that contain spaces, quotes, or other special characters.
12+
*
13+
* @param {string[]} args - Array of command arguments to escape.
14+
* @returns {string[]} - Array of escaped arguments suitable for shell execution.
15+
*/
16+
export function escapeCliArgs(args: string[]): string[] {
17+
return args.map(arg => {
18+
if (arg.includes(' ') || arg.includes('"') || arg.includes("'")) {
19+
return `"${arg.replace(/"/g, '\\"')}"`;
20+
}
21+
return arg;
22+
});
23+
}
24+
25+
/**
26+
* Formats environment variable values for display by stripping quotes and then escaping.
27+
*
28+
* @param {string} value - Environment variable value to format.
29+
* @returns {string} - Formatted and escaped value suitable for display.
30+
*/
31+
export function formatEnvValue(value: string): string {
32+
// Strip quotes from the value for display
33+
const cleanValue = value.replace(/"/g, '');
34+
return escapeCliArgs([cleanValue])[0] ?? cleanValue;
35+
}
36+
37+
/**
38+
* Builds a command string by escaping arguments that contain spaces, quotes, or other special characters.
39+
*
40+
* @param {string} command - The base command to execute.
41+
* @param {string[]} args - Array of command arguments.
42+
* @returns {string} - The complete command string with properly escaped arguments.
43+
*/
44+
export function buildCommandString(
45+
command: string,
46+
args: string[] = [],
47+
): string {
48+
if (args.length === 0) {
49+
return command;
50+
}
51+
52+
return `${command} ${escapeCliArgs(args).join(' ')}`;
53+
}
54+
55+
/**
56+
* Options for formatting a command log.
57+
*/
58+
export interface FormatCommandLogOptions {
59+
command: string;
60+
args?: string[];
61+
cwd?: string;
62+
env?: Record<string, string>;
63+
}
64+
65+
/**
66+
* Formats a command string with optional cwd prefix, environment variables, and ANSI colors.
67+
*
68+
* @param {FormatCommandLogOptions} options - Command formatting options.
69+
* @returns {string} - ANSI-colored formatted command string.
70+
*/
71+
export function formatCommandLog(options: FormatCommandLogOptions): string {
72+
const { command, args = [], cwd = process.cwd(), env } = options;
73+
const relativeDir = path.relative(process.cwd(), cwd);
74+
75+
return [
76+
...(relativeDir && relativeDir !== '.'
77+
? [ansis.italic(ansis.gray(relativeDir))]
78+
: []),
79+
ansis.yellow('$'),
80+
...(env && Object.keys(env).length > 0
81+
? Object.entries(env).map(([key, value]) => {
82+
return ansis.gray(`${key}=${formatEnvValue(value)}`);
83+
})
84+
: []),
85+
ansis.gray(command),
86+
ansis.gray(escapeCliArgs(args).join(' ')),
87+
].join(' ');
88+
}
89+
90+
/**
91+
* Converts an object with different types of values into an array of command-line arguments.
92+
*
93+
* @example
94+
* const args = objectToCliArgs({
95+
* _: ['node', 'index.js'], // node index.js
96+
* name: 'Juanita', // --name=Juanita
97+
* formats: ['json', 'md'] // --format=json --format=md
98+
* });
99+
*/
100+
export function objectToCliArgs<
101+
T extends object = Record<string, ArgumentValue>,
102+
>(params?: CliArgsObject<T>): string[] {
103+
if (!params) {
104+
return [];
105+
}
106+
107+
return Object.entries(params).flatMap(([key, value]) => {
108+
// process/file/script
109+
if (key === '_') {
110+
return Array.isArray(value) ? value : [`${value}`];
111+
}
112+
const prefix = key.length === 1 ? '-' : '--';
113+
// "-*" arguments (shorthands)
114+
if (Array.isArray(value)) {
115+
return value.map(v => `${prefix}${key}="${v}"`);
116+
}
117+
// "--*" arguments ==========
118+
119+
if (typeof value === 'object') {
120+
return Object.entries(value as Record<string, ArgumentValue>).flatMap(
121+
// transform nested objects to the dot notation `key.subkey`
122+
([k, v]) => objectToCliArgs({ [`${key}.${k}`]: v }),
123+
);
124+
}
125+
126+
if (typeof value === 'string') {
127+
return [`${prefix}${key}="${value}"`];
128+
}
129+
130+
if (typeof value === 'number') {
131+
return [`${prefix}${key}=${value}`];
132+
}
133+
134+
if (typeof value === 'boolean') {
135+
return [`${prefix}${value ? '' : 'no-'}${key}`];
136+
}
137+
138+
if (typeof value === 'undefined') {
139+
return [];
140+
}
141+
142+
throw new Error(`Unsupported type ${typeof value} for key ${key}`);
143+
});
144+
}
145+
146+
/**
147+
* Converts a file path to a CLI argument by wrapping it in quotes to handle spaces.
148+
*
149+
* @param {string} filePath - The file path to convert to a CLI argument.
150+
* @returns {string} - The quoted file path suitable for CLI usage.
151+
*/
152+
export function filePathToCliArg(filePath: string): string {
153+
// needs to be escaped if spaces included
154+
return `"${filePath}"`;
155+
}

0 commit comments

Comments
 (0)