Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(cli): prepare for core package #80

Merged
merged 5 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"bundle-require": "^4.0.1",
"chalk": "^5.3.0",
"yargs": "^17.7.2",
"zod": "^3.22.1",
"@quality-metrics/models": "^0.0.1",
"@quality-metrics/utils": "^0.0.1"
"@quality-metrics/utils": "^0.0.1",
"zod": "^3.22.1"
}
}
17 changes: 9 additions & 8 deletions packages/cli/src/lib/cli.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { join } from 'path';
import { describe, expect, it } from 'vitest';
import { yargsCli } from './cli';
import { getDirname } from './implementation/utils';
import { middlewares } from './middlewares';
import { options as defaultOptions } from './options';
import { CommandBase } from './implementation/model';
import { CollectOptions } from '@quality-metrics/utils';
import { GlobalOptions } from './model';
import { getDirname } from './implementation/helper.mock';

const __dirname = getDirname(import.meta.url);
const withDirName = (path: string) => join(__dirname, path);
Expand All @@ -16,10 +17,10 @@ const demandCommand: [number, string] = [0, 'no command required'];
describe('CLI arguments parsing', () => {
it('options should provide correct defaults', async () => {
const args: string[] = [];
const parsedArgv: CommandBase = yargsCli(args, {
const parsedArgv = yargsCli(args, {
options,
demandCommand,
}).argv;
}).argv as unknown as GlobalOptions;
expect(parsedArgv.configPath).toContain('code-pushup.config.js');
expect(parsedArgv.verbose).toBe(false);
expect(parsedArgv.interactive).toBe(true);
Expand All @@ -33,21 +34,21 @@ describe('CLI arguments parsing', () => {
validConfigPath,
];

const parsedArgv: CommandBase = yargsCli(args, {
const parsedArgv = yargsCli(args, {
options,
demandCommand,
}).argv;
}).argv as unknown as GlobalOptions & CollectOptions;
expect(parsedArgv.configPath).toContain(validConfigPath);
expect(parsedArgv.verbose).toBe(true);
expect(parsedArgv.interactive).toBe(false);
});

it('middleware should use config correctly', async () => {
const args: string[] = ['--configPath', validConfigPath];
const parsedArgv: CommandBase = await yargsCli(args, {
const parsedArgv = (await yargsCli(args, {
demandCommand,
middlewares,
}).argv;
}).argv) as unknown as GlobalOptions & CollectOptions;
expect(parsedArgv.configPath).toContain(validConfigPath);
expect(parsedArgv.persist.outputPath).toContain('cli-config-out.json');
});
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/lib/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CoreConfig } from '@quality-metrics/models';
import { CoreConfig, GlobalOptions } from '@quality-metrics/models';
import chalk from 'chalk';
import yargs, {
Argv,
Expand Down Expand Up @@ -30,7 +30,7 @@ export function yargsCli(
applyBeforeValidation?: boolean;
}[];
},
): Argv<CoreConfig> {
): Argv<CoreConfig & GlobalOptions> {
const { usageMessage, scriptName } = cfg;
let { commands, options, middlewares /*demandCommand*/ } = cfg;
// demandCommand = Array.isArray(demandCommand) ? demandCommand: [1, 'Minimum 1 command!']; @TODO implement when commands are present
Expand Down Expand Up @@ -81,5 +81,5 @@ export function yargsCli(
});

// return CLI object
return cli as unknown as Argv<CoreConfig>;
return cli as unknown as Argv<CoreConfig & GlobalOptions>;
}
3 changes: 2 additions & 1 deletion packages/cli/src/lib/collect/command-object.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { CollectOptions } from '@quality-metrics/utils';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { yargsCli } from '../cli';
import { getDirname, logErrorBeforeThrow } from '../implementation/utils';
import { logErrorBeforeThrow } from '../implementation/utils';
import { middlewares } from '../middlewares';
import { yargsGlobalOptionsDefinition } from '../implementation/global-options';
import { yargsCollectCommandObject } from './command-object';
import { getDirname } from '../implementation/helper.mock';

const command = {
...yargsCollectCommandObject(),
Expand Down
37 changes: 2 additions & 35 deletions packages/cli/src/lib/collect/command-object.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,10 @@
import { pluginOutputSchema, Report } from '@quality-metrics/models';
import {
collect,
CollectOptions,
CollectOutputError,
persistReport,
logPersistedResults,
} from '@quality-metrics/utils';
import { CommandModule } from 'yargs';
import * as packageJson from '../../../package.json';
import { collectAndPersistReports } from '../implementation/collect-and-persist';

export function yargsCollectCommandObject() {
const handler = async (
config: CollectOptions & { format: string },
): Promise<void> => {
const collectReport = await collect(config);
const report: Report = {
...collectReport,
packageName: packageJson.name,
version: packageJson.version,
};

const persistResults = await persistReport(report, config);

logPersistedResults(persistResults);

// validate report
report.plugins.forEach(plugin => {
try {
// Running checks after persisting helps while debugging as you can check the invalid output after the error
pluginOutputSchema.parse(plugin);
} catch (e) {
throw new CollectOutputError(plugin.slug, e as Error);
}
});
};

return {
command: 'collect',
describe: 'Run Plugins and collect results',
handler: handler as unknown as CommandModule['handler'],
handler: collectAndPersistReports as unknown as CommandModule['handler'],
} satisfies CommandModule;
}
35 changes: 35 additions & 0 deletions packages/cli/src/lib/implementation/collect-and-persist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
collect,
CollectOptions,
CollectOutputError,
logPersistedResults,
persistReport,
} from '@quality-metrics/utils';
import { pluginOutputSchema, Report } from '@quality-metrics/models';
import * as packageJson from '../../../package.json';

// @TODO [73] move into core
export async function collectAndPersistReports(
config: CollectOptions,
): Promise<void> {
const collectReport = await collect(config);
const report: Report = {
...collectReport,
packageName: packageJson.name,
version: packageJson.version,
};

const persistResults = await persistReport(report, config);

logPersistedResults(persistResults);

// validate report
report.plugins.forEach(plugin => {
try {
// Running checks after persisting helps while debugging as you can check the invalid output after the error
pluginOutputSchema.parse(plugin);
} catch (e) {
throw new CollectOutputError(plugin.slug, e as Error);
}
});
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { join } from 'path';
import { expect } from 'vitest';
import { configMiddleware, ConfigParseError } from './config-middleware';
import { getDirname } from './utils';
import { getDirname } from './helper.mock';

const __dirname = getDirname(import.meta.url);

Expand Down
35 changes: 8 additions & 27 deletions packages/cli/src/lib/implementation/config-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
import { bundleRequire } from 'bundle-require';
import { stat } from 'fs/promises';
import { GlobalOptions, globalOptionsSchema } from '../model';
import { CommandBase, commandBaseSchema } from './model';
import { readCodePushupConfig } from './read-code-pushup-config';

export class ConfigParseError extends Error {
constructor(configPath: string) {
super(`Config file ${configPath} does not exist`);
}
}

export async function configMiddleware<T = unknown>(
processArgs: T,
): Promise<CommandBase> {
const globalCfg: GlobalOptions = globalOptionsSchema.parse(processArgs);
const { configPath } = globalCfg;
try {
const stats = await stat(configPath);
if (!stats.isFile) {
throw new ConfigParseError(configPath);
}
} catch (err) {
throw new ConfigParseError(configPath);
}

const { mod } = await bundleRequire({
filepath: globalCfg.configPath,
format: 'esm',
});
const exportedConfig = mod.default || mod;

return commandBaseSchema.parse({
...globalCfg,
...exportedConfig,
export async function configMiddleware<T = unknown>(processArgs: T) {
const globalOptions: GlobalOptions = globalOptionsSchema.parse(processArgs);
const importedRc = await readCodePushupConfig(globalOptions.configPath);
return {
...importedRc,
...processArgs,
});
...globalOptions,
};
}
5 changes: 5 additions & 0 deletions packages/cli/src/lib/implementation/helper.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { dirname } from 'path';
import { fileURLToPath } from 'url';

export const getDirname = (import_meta_url: string) =>
dirname(fileURLToPath(import_meta_url));
15 changes: 15 additions & 0 deletions packages/cli/src/lib/implementation/load-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { bundleRequire, Options } from 'bundle-require';

// @TODO [73] move into utils
export async function importModule<T = unknown>(
options: Options,
parse?: (d: unknown) => T,
) {
parse = parse || (v => v as T);
options = {
format: 'esm',
...options,
};
const { mod } = await bundleRequire(options);
return parse(mod.default || mod);
}
23 changes: 23 additions & 0 deletions packages/cli/src/lib/implementation/read-code-pushup-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CoreConfig, coreConfigSchema } from '@quality-metrics/models';
import { stat } from 'fs/promises';
import { importModule } from './load-file';
import { ConfigParseError } from './config-middleware';

// @TODO [73] move into core
export async function readCodePushupConfig(filepath: string) {
try {
const stats = await stat(filepath);
if (!stats.isFile) {
throw new ConfigParseError(filepath);
}
} catch (err) {
throw new ConfigParseError(filepath);
}

return importModule<CoreConfig>(
{
filepath,
},
coreConfigSchema.parse,
);
}
8 changes: 1 addition & 7 deletions packages/cli/src/lib/implementation/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { dirname } from 'path';
import { fileURLToPath } from 'url';

export const getDirname = (import_meta_url: string) =>
dirname(fileURLToPath(import_meta_url));

// log error and flush stdout so that Yargs doesn't supress it
// log error and flush stdout so that Yargs doesn't suppress it
// related issue: https://github.com/yargs/yargs/issues/2118
export function logErrorBeforeThrow<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
9 changes: 4 additions & 5 deletions packages/cli/src/lib/middlewares.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { MiddlewareFunction } from 'yargs';
import { configMiddleware } from './implementation/config-middleware';
import { MiddlewareFunction } from 'yargs';

export const middlewares: {
middlewareFunction: MiddlewareFunction;
applyBeforeValidation?: boolean;
}[] = [{ middlewareFunction: configMiddleware }];
export const middlewares = [
{ middlewareFunction: configMiddleware as unknown as MiddlewareFunction },
];
1 change: 0 additions & 1 deletion packages/models/src/lib/plugin-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ describe('pluginConfigSchema', () => {

it('should throw if a group has duplicate audit refs', () => {
const auditSlug = 'no-any';
// @TODO use pluginConfigSchema instead of auditGroupSchema
const cfg = mockGroupConfig({ auditSlug: [auditSlug, auditSlug] });

expect(() => auditGroupSchema.parse(cfg)).toThrow(
Expand Down
16 changes: 11 additions & 5 deletions packages/utils/src/lib/collect/implementation/execute-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class PluginOutputError extends Error {
* Execute a plugin.
*
* @public
* @param cfg - {@link ProcessConfig} object with runner and meta
* @param pluginConfig - {@link ProcessConfig} object with runner and meta
* @param observer - process {@link ProcessObserver}
* @returns {Promise<AuditOutput[]>} - audit outputs from plugin runner
* @throws {PluginOutputError} - if plugin runner output is invalid
Expand All @@ -45,11 +45,12 @@ export class PluginOutputError extends Error {
* }
*/
export async function executePlugin(
cfg: PluginConfig,
pluginConfig: PluginConfig,
observer?: ProcessObserver,
): Promise<PluginOutput> {
const { slug, title, description, docsUrl } = cfg;
const { args, command } = cfg.runner;
const { slug, title, description, docsUrl, version, packageName } =
pluginConfig;
const { args, command } = pluginConfig.runner;

const { duration, date } = await executeProcess({
command,
Expand All @@ -58,13 +59,18 @@ export async function executePlugin(
});

try {
const processOutputPath = join(process.cwd(), cfg.runner.outputPath);
const processOutputPath = join(
process.cwd(),
pluginConfig.runner.outputPath,
);
// read process output from file system and parse it
const audits = auditOutputsSchema.parse(
JSON.parse((await readFile(processOutputPath)).toString()),
);

return {
version,
packageName,
slug,
title,
description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('executeProcess', () => {
expect(observer?.complete).toHaveBeenCalledTimes(1);
});

it('should work with async script `node custom-script.js` that throws an error', async () => {
it('should work with async script `node custom-script.js --arg` that throws an error', async () => {
const cfg = mockProcessConfig(
getAsyncProcessRunnerConfig({ interval: 10, runs: 1, throwError: true }),
);
Expand Down
Loading