-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
1,078 additions
and
183 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
66
packages/cli/src/lib/implemetation/config-middleware.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
25
packages/cli/src/lib/implemetation/mock/cli-config.mock.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: [], | ||
}; |
5 changes: 5 additions & 0 deletions
5
packages/cli/src/lib/implemetation/mock/config-middleware-config.mock.cjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
persist: { outputPath: 'cjs-out.json' }, | ||
plugins: [], | ||
categories: [], | ||
}; |
5 changes: 5 additions & 0 deletions
5
packages/cli/src/lib/implemetation/mock/config-middleware-config.mock.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
persist: { outputPath: 'js-out.json' }, | ||
plugins: [], | ||
categories: [], | ||
}; |
5 changes: 5 additions & 0 deletions
5
packages/cli/src/lib/implemetation/mock/config-middleware-config.mock.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export default { | ||
persist: { outputPath: 'mjs-out.json' }, | ||
plugins: [], | ||
categories: [], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}; | ||
} |