Skip to content

Commit

Permalink
feat(nx-plugin): add general executor logic
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton committed Jul 12, 2024
1 parent 2f8d78e commit 3b5b977
Show file tree
Hide file tree
Showing 28 changed files with 1,129 additions and 91 deletions.
1 change: 1 addition & 0 deletions packages/core/src/lib/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type UploadOptions = { upload?: UploadConfig } & {
/**
* Uploads collected audits to the portal
* @param options
* @param uploadFn
*/
export async function upload(
options: UploadOptions,
Expand Down
6 changes: 4 additions & 2 deletions packages/nx-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

#### Init

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

Examples:

Expand All @@ -13,7 +14,8 @@ Examples:

#### Configuration

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

Examples:

Expand Down
7 changes: 7 additions & 0 deletions packages/nx-plugin/mock/fixtures/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const ENV = {
CP_SERVER: 'https://portal.code.pushup.dev',
CP_ORGANIZATION: 'code-pushup',
CP_PROJECT: 'utils',
CP_API_KEY: '23456789098765432345678909876543',
CP_TIMEOUT: 9,
};
5 changes: 4 additions & 1 deletion packages/nx-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"dependencies": {
"@nx/devkit": "^17.1.3",
"tslib": "2.6.2",
"nx": "^17.1.3"
"nx": "^17.1.3",
"@code-pushup/models": "*",
"@code-pushup/utils": "*",
"zod": "^3.22.4"
},
"type": "commonjs",
"main": "./src/index.js",
Expand Down
62 changes: 62 additions & 0 deletions packages/nx-plugin/src/executors/internal/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { CliArgsObject } from '@code-pushup/utils';

export function createCliCommand(
command: string,
args: Record<string, unknown>,
): string {
return `npx @code-pushup/cli ${command} ${objectToCliArgs(args).join(' ')}`;
}

type ArgumentValue = number | string | boolean | string[];
// @TODO import from @code-pushup/utils => get rid of poppins for cjs support
// eslint-disable-next-line sonarjs/cognitive-complexity
export function objectToCliArgs<
T extends object = Record<string, ArgumentValue>,
>(params?: CliArgsObject<T>): string[] {
if (!params) {
return [];
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Object.entries(params).flatMap(([key, value]) => {
// process/file/script
if (key === '_') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Array.isArray(value) ? value : [`${value}`];
}
const prefix = key.length === 1 ? '-' : '--';
// "-*" arguments (shorthands)
if (Array.isArray(value)) {
return value.map(v => `${prefix}${key}="${v}"`);
}
// "--*" arguments ==========

if (Array.isArray(value)) {
return value.map(v => `${prefix}${key}="${v}"`);
}

if (typeof value === 'object') {
return Object.entries(value as Record<string, unknown>).map(
([k, v]) => `${prefix}${key}.${k}="${v?.toString()}"`,
);
}

if (typeof value === 'string') {
return [`${prefix}${key}="${value}"`];
}

if (typeof value === 'number') {
return [`${prefix}${key}=${value}`];
}

if (typeof value === 'boolean') {
return [`${prefix}${value ? '' : 'no-'}${key}`];
}

if (value === undefined) {
return [];
}

throw new Error(`Unsupported type ${typeof value} for key ${key}`);
});
}
66 changes: 66 additions & 0 deletions packages/nx-plugin/src/executors/internal/cli.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { objectToCliArgs } from './cli';

describe('objectToCliArgs', () => {
it('should handle the "_" argument as script', () => {
const params = { _: 'bin.js' };
const result = objectToCliArgs(params);
expect(result).toEqual(['bin.js']);
});

it('should handle the "_" argument with multiple values', () => {
const params = { _: ['bin.js', '--help'] };
const result = objectToCliArgs(params);
expect(result).toEqual(['bin.js', '--help']);
});

it('should handle shorthands arguments', () => {
const params = {
e: `test`,
};
const result = objectToCliArgs(params);
expect(result).toEqual([`-e="${params.e}"`]);
});

it('should handle string arguments', () => {
const params = { name: 'Juanita' };
const result = objectToCliArgs(params);
expect(result).toEqual(['--name="Juanita"']);
});

it('should handle number arguments', () => {
const params = { parallel: 5 };
const result = objectToCliArgs(params);
expect(result).toEqual(['--parallel=5']);
});

it('should handle boolean arguments', () => {
const params = { progress: true };
const result = objectToCliArgs(params);
expect(result).toEqual(['--progress']);
});

it('should handle negated boolean arguments', () => {
const params = { progress: false };
const result = objectToCliArgs(params);
expect(result).toEqual(['--no-progress']);
});

it('should handle array of string arguments', () => {
const params = { format: ['json', 'md'] };
const result = objectToCliArgs(params);
expect(result).toEqual(['--format="json"', '--format="md"']);
});

it('should handle objects', () => {
const params = { format: { json: 'simple' } };
const result = objectToCliArgs(params);
expect(result).toStrictEqual(['--format.json="simple"']);
});

it('should throw error for unsupported type', () => {
expect(() => objectToCliArgs({ param: Symbol('') })).toThrow(
'Unsupported type',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect } from 'vitest';
import { ENV } from '../../../mock/fixtures/env';
import { uploadConfig } from './config';
import { parseEnv } from './env';

vi.mock('./env', async () => {
const actual = await vi.importActual('./env');
return {
...actual,
parseEnv: vi.fn(actual.parseEnv as typeof parseEnv),
};
});

describe('uploadConfig', () => {
it('should call parseEnv function with values from process.env', () => {
const old = process.env;

// eslint-disable-next-line functional/immutable-data
process.env = ENV;

expect(
uploadConfig(
{
server: 'https://portal.code.pushup.dev',
},
{
workspaceRoot: 'workspaceRoot',
projectConfig: {
name: 'my-app',
root: 'root',
},
},
),
).toEqual(
expect.objectContaining({
server: ENV.CP_SERVER,
apiKey: ENV.CP_API_KEY,
organization: ENV.CP_ORGANIZATION,
project: ENV.CP_PROJECT,
}),
);

expect(parseEnv).toHaveBeenCalledTimes(1);
expect(parseEnv).toHaveBeenCalledWith(ENV);

// eslint-disable-next-line functional/immutable-data
process.env = old;
});
});
71 changes: 71 additions & 0 deletions packages/nx-plugin/src/executors/internal/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { join } from 'node:path';
import type { PersistConfig, UploadConfig } from '@code-pushup/models';
import { slugify } from '../../internal/utils';
import { parseEnv } from './env';
import {
BaseNormalizedExecutorContext,
GlobalExecutorOptions,
ProjectExecutorOnlyOptions,
} from './types';

export function globalConfig(
options: Partial<GlobalExecutorOptions>,
context: BaseNormalizedExecutorContext,
): Required<GlobalExecutorOptions> {
const { projectConfig } = context;
const { root: projectRoot = '' } = projectConfig ?? {};
// For better debugging use `--verbose --no-progress` as default
const { verbose, progress, config } = options;
return {
verbose: !!verbose,
progress: !!progress,
config: config ?? join(projectRoot, 'code-pushup.config.json'),
};
}

export function persistConfig(
options: Partial<PersistConfig & ProjectExecutorOnlyOptions>,
context: BaseNormalizedExecutorContext,
): Partial<PersistConfig> {
const { projectConfig } = context;

const { name: projectName = '', root: projectRoot = '' } =
projectConfig ?? {};
const {
format = ['json'],
outputDir = join(projectRoot, '.code-pushup', projectName), // always in <root>/.code-pushup/<project-name>,
filename: filenameOptions,
} = options;

return {
format,
outputDir,
...(filenameOptions ? { filename: slugify(filenameOptions) } : {}),
};
}

export function uploadConfig(
options: Partial<UploadConfig & ProjectExecutorOnlyOptions>,
context: BaseNormalizedExecutorContext,
): Partial<UploadConfig> {
const { projectConfig, workspaceRoot } = context;

const { name: projectName } = projectConfig ?? {};
const { projectPrefix, server, apiKey, organization, project, timeout } =
options;
const applyPrefix = workspaceRoot === '.';
const prefix = projectPrefix ? `${projectPrefix}-` : '';
return {
...(projectName
? {
project: applyPrefix ? `${prefix}${projectName}` : projectName, // provide correct project
}
: {}),
...parseEnv(process.env),
...Object.fromEntries(
Object.entries({ server, apiKey, organization, project, timeout }).filter(
([_, v]) => v !== undefined,
),
),
};
}
Loading

0 comments on commit 3b5b977

Please sign in to comment.