Skip to content

Commit

Permalink
feat(cli): rearrange files
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton committed Sep 7, 2023
1 parent adec416 commit ff7e114
Show file tree
Hide file tree
Showing 15 changed files with 1,078 additions and 183 deletions.
919 changes: 754 additions & 165 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"dist/packages/*"
],
"dependencies": {
"@types/yargs": "^17.0.24",
"@types/yargs-parser": "^21.0.0",
"bundle-require": "^4.0.1",
"chalk": "^5.3.0",
"yargs": "^17.7.2",
"zod": "^3.22.1"
},
"devDependencies": {
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#! /usr/bin/env node
import { hideBin } from 'yargs/helpers';
import { yargsCli } from './lib/cli';
import { yargsGlobalOptionsDefinition } from './lib/options';
import { middlewares } from './lib/middlewares';
import { commands } from './lib/commands';

yargsCli({
usageMessage: 'CPU CLI',
scriptName: 'cpu',
options: yargsGlobalOptionsDefinition(),
middlewares,
commands
})
// bootstrap yargs; format arguments
.parseAsync(hideBin(process.argv));
15 changes: 5 additions & 10 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { pathToFileURL } from 'url';
import { cli } from './lib/cli';
import { coreConfigSchema, globalCliArgsSchema } from '@quality-metrics/models';
import { z } from 'zod';

export { cli };

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
if (!process.argv[2]) {
throw new Error('Missing config file path');
}
cli(process.argv[2]).then(console.log).catch(console.error);
}
export { yargsCli } from './lib/cli';
export const baseCommandSchema = globalCliArgsSchema.merge(coreConfigSchema);
export type BaseCommandSchema = z.infer<typeof baseCommandSchema>;
58 changes: 58 additions & 0 deletions packages/cli/src/lib/cli.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {describe, expect, it} from 'vitest';

import {existsSync} from 'fs';
import {yargsCli} from './cli';
import {join} from 'path';
import {yargsGlobalOptionsDefinition} from './options';
import {commands} from './commands';
import {middlewares} from './middlewares';

const withDirName = (path: string) => join(__dirname, path);
const validConfigPath = withDirName('mock/cli-config.mock.js');

const options = yargsGlobalOptionsDefinition();

describe('cli', () => {
it('options should provide correct defaults', async () => {
const args: string[] = [];
const parsedArgv = yargsCli<any>({ options }).parse(args);
expect(parsedArgv.configPath).toContain('cpu-config.js');
expect(parsedArgv.verbose).toBe(false);
expect(parsedArgv.interactive).toBe(true);
});

it('options should parse correctly', async () => {
const args: any[] = [
'--verbose',
'--no-interactive',
'--configPath',
validConfigPath,
];

const parsedArgv: any = yargsCli({ options }).parse(args);
console.log('argv: ', parsedArgv);
expect(parsedArgv.configPath).toContain(validConfigPath);
expect(parsedArgv.verbose).toBe(true);
expect(parsedArgv.interactive).toBe(false);
});

it('middleware should use config correctly', async () => {
const args: any[] = ['--configPath', validConfigPath];
const parsedArgv: any = await yargsCli({
middlewares: middlewares as any,
}).parseAsync(args);
expect(parsedArgv.configPath).toContain('cli-config.mock.js');
expect(parsedArgv.persist.outputPath).toContain('cli-config-out.json');
});

it('run commands should execute correctly', async () => {
const args: any[] = ['run', '--configPath', validConfigPath];
const parsedArgv: any = await yargsCli({
commands: commands,
middlewares: middlewares,
}).parseAsync(args);
expect(parsedArgv.configPath).toContain('cli-config.mock.js');
expect(parsedArgv.persist.outputPath).toContain('cli-config-out.json');
expect(existsSync(parsedArgv.persist.outputPath)).toBeTruthy();
});
});
56 changes: 48 additions & 8 deletions packages/cli/src/lib/cli.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,49 @@
import { bundleRequire } from 'bundle-require';

export async function cli(configPath: string) {
const { mod } = await bundleRequire({
filepath: configPath,
format: 'esm',
});
return mod.default || mod;
import yargs, {Argv, CommandModule, MiddlewareFunction, Options, ParserConfigurationOptions,} from 'yargs';
import chalk from 'chalk';

/**
* returns configurable yargs cli
* @example
* // bootstrap yargs; format arguments
* yargsCli().parse(hideBin(process.argv));
*
*/
export function yargsCli<T>(cfg: {
usageMessage?: string;
scriptName?: string;
commands?: CommandModule[];
options?: { [key: string]: Options };
middlewares?: {
middlewareFunction: MiddlewareFunction;
applyBeforeValidation?: boolean;
}[];
}): Argv<T> {
const { usageMessage, scriptName } = cfg;
let { commands, options, middlewares } = cfg;
commands = Array.isArray(commands) ? commands : [];
middlewares = Array.isArray(middlewares) ? middlewares : [];
options = options || {};
const cli = yargs();

// setup yargs
cli
.parserConfiguration({
'strip-dashed': true,
} satisfies Partial<ParserConfigurationOptions>)
.options(options);
//.demandCommand(1, 'Minimum 1 command!')

usageMessage ? cli.usage(chalk.bold(usageMessage)) : void 0;
scriptName ? cli.scriptName(scriptName) : void 0;

// add middlewares
middlewares.forEach(({ middlewareFunction, applyBeforeValidation }) =>
cli.middleware(middlewareFunction, applyBeforeValidation),
);

// add commands
commands.forEach(commandObj => cli.command(commandObj));

// return CLI object
return cli;
}
3 changes: 3 additions & 0 deletions packages/cli/src/lib/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CommandModule } from 'yargs';

export const commands: CommandModule[] = [];
66 changes: 66 additions & 0 deletions packages/cli/src/lib/implemetation/config-middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { join } from 'path';
import {
applyConfigMiddlewareToHandler,
ConfigParseError,
} from './config-middleware';
import { BaseCommandSchema } from '../../index';
import { expect } from 'vitest';

const withDirName = (path: string) => join(__dirname, path);

describe('applyConfigMiddleware', () => {
it('should load valid config `read-config.mock.mjs`', async () => {
const configPathMjs = withDirName('mock/config-middleware-config.mock.mjs');
const calledWith: BaseCommandSchema[] = [];
const adoptedHandler = applyConfigMiddlewareToHandler(async args => {
calledWith.push(args);
});

await adoptedHandler({ configPath: configPathMjs });
expect(calledWith.length).toBe(1);
expect(calledWith[0]?.configPath).toContain('.mjs');
expect(calledWith[0]?.persist.outputPath).toContain('mjs-');
});

it('should load valid config `read-config.mock.cjs`', async () => {
const configPathCjs = withDirName('mock/config-middleware-config.mock.cjs');
const calledWith: BaseCommandSchema[] = [];
const adoptedHandler = applyConfigMiddlewareToHandler(async args => {
calledWith.push(args);
});

await adoptedHandler({ configPath: configPathCjs });
expect(calledWith.length).toBe(1);
expect(calledWith[0]?.configPath).toContain('.cjs');
expect(calledWith[0]?.persist.outputPath).toContain('cjs-');
});

it('should load valid config `read-config.mock.js`', async () => {
const configPathJs = withDirName('mock/config-middleware-config.mock.js');
const calledWith: BaseCommandSchema[] = [];
const adoptedHandler = applyConfigMiddlewareToHandler(async args => {
calledWith.push(args);
return void 0;
});
await adoptedHandler({ configPath: configPathJs });
expect(calledWith.length).toBe(1);
expect(calledWith[0]?.configPath).toContain('.js');
expect(calledWith[0]?.persist.outputPath).toContain('js-');
});

it('should throws with invalid configPath', async () => {
const z = applyConfigMiddlewareToHandler(async () => void 0);
const configPath = 'wrong/path/to/config';
expect(() => z({ configPath })).toThrowError(
new ConfigParseError(configPath),
);
});

it('should provide default configPath', async () => {
const z = applyConfigMiddlewareToHandler(async () => void 0);
const defaultConfigPath = 'cpu-config.js';
expect(() => z({ configPath: undefined })).toThrowError(
new ConfigParseError(defaultConfigPath),
);
});
});
55 changes: 55 additions & 0 deletions packages/cli/src/lib/implemetation/config-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { GlobalCliArgsSchema, globalCliArgsSchema } from '@quality-metrics/models';
import { BaseCommandSchema, baseCommandSchema } from '../../index';
import { existsSync } from 'node:fs';
import { ArgumentsCamelCase } from 'yargs';
import {bundleRequire} from "bundle-require";

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

export function applyConfigMiddlewareToHandler(
handler: (processArgs: BaseCommandSchema) => Promise<void>,
) {
return (processArgs: Partial<GlobalCliArgsSchema>): Promise<void> => {
const globalCfg: GlobalCliArgsSchema =
globalCliArgsSchema.parse(processArgs);

if (!existsSync(globalCfg.configPath)) {
throw new ConfigParseError(globalCfg.configPath);
}

return import(globalCfg.configPath)
.then(m => m.default)
.then(exportedConfig => {
const configFromFile = baseCommandSchema.parse({
...globalCfg,
...exportedConfig,
});
return handler(configFromFile);
});
};
}

export async function configMiddleware<T = Record<string, any>>(
processArgs: ArgumentsCamelCase<T>,
) {
const globalCfg: GlobalCliArgsSchema = globalCliArgsSchema.parse(processArgs);
const {configPath} = globalCfg;
if (!existsSync(configPath)) {
throw new ConfigParseError(configPath);
}

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

return baseCommandSchema.parse({
...globalCfg,
...exportedConfig,
});
}
25 changes: 25 additions & 0 deletions packages/cli/src/lib/implemetation/mock/cli-config.mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
persist: { outputPath: 'cli-config-out.json' },
plugins: [
{
audits: [],
runner: {
command: 'bash',
args: [
'-c',
`echo '${JSON.stringify({
audits: [],
})}' > cli-config-out.json`,
],
outputPath: 'cli-config-out.json',
},
groups: [],
meta: {
slug: 'execute-plugin',
name: 'execute plugin',
type: 'static-analysis',
},
},
],
categories: [],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
persist: { outputPath: 'cjs-out.json' },
plugins: [],
categories: [],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
persist: { outputPath: 'js-out.json' },
plugins: [],
categories: [],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
persist: { outputPath: 'mjs-out.json' },
plugins: [],
categories: [],
};
7 changes: 7 additions & 0 deletions packages/cli/src/lib/middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { configMiddleware } from './implemetation/config-middleware';
import { MiddlewareFunction } from 'yargs';

export const middlewares: {
middlewareFunction: MiddlewareFunction;
applyBeforeValidation?: boolean;
}[] = [{ middlewareFunction: configMiddleware as any }];
22 changes: 22 additions & 0 deletions packages/cli/src/lib/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Options } from 'yargs';

export function yargsGlobalOptionsDefinition(): Record<string, Options> {
return {
interactive: {
describe: 'When false disables interactive input prompts for options.',
type: 'boolean',
default: true,
},
verbose: {
describe:
'When true creates more verbose output. This is helpful when debugging.',
type: 'boolean',
default: false,
},
configPath: {
describe: 'Path the the config file. e.g. cpu-config.js',
type: 'string',
default: 'cpu-config.js',
},
};
}

0 comments on commit ff7e114

Please sign in to comment.