Switching to yargs-parser rather than yargs (#356)

* totally changing how args are parsed with yargs-parser

* fix up initCommand with new yargs types

* Change files

* fixing up resolve

* making the watch script to be consistent

* fixing the mocks with yargs init
This commit is contained in:
Kenneth Chau 2020-04-10 13:31:08 -07:00 коммит произвёл GitHub
Родитель b57d6acd84
Коммит df664ff35c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 247 добавлений и 212 удалений

Просмотреть файл

@ -0,0 +1,8 @@
{
"type": "none",
"comment": "fix up initCommand with new yargs types",
"packageName": "create-just",
"email": "kchau@microsoft.com",
"commit": "565e98494de5bae050cdc04365dee0a0aef638d8",
"date": "2020-04-10T04:31:08.848Z"
}

Просмотреть файл

@ -0,0 +1,8 @@
{
"type": "patch",
"comment": "Jest now can take a positional arg to run a certain test pattern",
"packageName": "just-scripts",
"email": "kchau@microsoft.com",
"commit": "565e98494de5bae050cdc04365dee0a0aef638d8",
"date": "2020-04-10T04:31:34.381Z"
}

Просмотреть файл

@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "Replace yargs with yargs-parser, taking over the command parsing",
"packageName": "just-task",
"email": "kchau@microsoft.com",
"commit": "565e98494de5bae050cdc04365dee0a0aef638d8",
"date": "2020-04-10T04:31:52.506Z"
}

Просмотреть файл

@ -39,7 +39,7 @@ async function getStackPath(pathName: string, registry?: string) {
* 4. git init and commit
* 5. yarn install
*/
export async function initCommand(argv: yargs.Arguments) {
export async function initCommand(argv: yargs.Arguments<{ [key: string]: string }>) {
// TODO: autosuggest just-stack-* packages from npmjs.org
if (!argv.stack) {
const { stack } = await prompts({

Просмотреть файл

@ -1,4 +1,4 @@
import { resolve, logger, resolveCwd, TaskFunction } from 'just-task';
import { resolve, logger, resolveCwd, TaskFunction, argv } from 'just-task';
import { spawn, encodeArgs } from 'just-scripts-utils';
import { existsSync } from 'fs';
import supportsColor from 'supports-color';
@ -11,6 +11,8 @@ export interface JestTaskOptions {
watch?: boolean;
colors?: boolean;
passWithNoTests?: boolean;
testPathPattern?: string;
testNamePattern?: string;
u?: boolean;
_?: string[];
@ -35,6 +37,10 @@ export function jestTask(options: JestTaskOptions = {}): TaskFunction {
if (configFile && jestCmd && existsSync(configFile)) {
logger.info(`Running Jest`);
const cmd = process.execPath;
const positional = argv()._.slice(1);
options = { ...options, ...{ ...argv(), _: positional } };
const args = [
...(options.nodeArgs || []),
jestCmd,
@ -45,6 +51,8 @@ export function jestTask(options: JestTaskOptions = {}): TaskFunction {
...(options.runInBand ? ['--runInBand'] : []),
...(options.coverage ? ['--coverage'] : []),
...(options.watch ? ['--watch'] : []),
...(options.testPathPattern ? ['--testPathPattern', options.testPathPattern] : []),
...(options.testNamePattern ? ['--testNamePattern', options.testNamePattern] : []),
...(options.u || options.updateSnapshot ? ['--updateSnapshot'] : ['']),
...(options._ || [])
].filter(arg => !!arg) as Array<string>;

Просмотреть файл

@ -15,19 +15,20 @@
},
"scripts": {
"build": "tsc",
"dev": "tsc -w --preserveWatchOutput",
"start": "tsc -w --preserveWatchOutput",
"start-test": "jest --watch",
"test": "jest"
},
"dependencies": {
"@microsoft/package-deps-hash": "^2.2.153",
"bach": "^1.2.0",
"chalk": "^2.4.1",
"fs-extra": "^7.0.1",
"just-task-logger": ">=0.3.0 <1.0.0",
"resolve": "^1.8.1",
"undertaker": "^1.2.0",
"undertaker": "^1.2.1",
"undertaker-registry": "^1.0.1",
"yargs": "^12.0.5"
"yargs-parser": "^18.1.2"
},
"devDependencies": {
"@types/fs-extra": "^5.0.4",
@ -37,7 +38,7 @@
"@types/resolve": "^0.0.8",
"@types/undertaker": "^1.2.0",
"@types/undertaker-registry": "^1.0.1",
"@types/yargs": "12.0.1",
"@types/yargs-parser": "^15.0.0",
"jest": "^24.0.0",
"mock-fs": "^4.8.0",
"ts-jest": "^24.0.1",

Просмотреть файл

@ -1,115 +0,0 @@
import yargs from 'yargs';
import fs from 'fs';
import path from 'path';
import { logger, mark } from './logger';
import UndertakerRegistry from 'undertaker-registry';
import Undertaker from 'undertaker';
import { resolve } from './resolve';
import { enableTypeScript } from './enableTypeScript';
export class JustTaskRegistry extends UndertakerRegistry {
private hasDefault: boolean = false;
public init(taker: Undertaker) {
super.init(taker);
// uses a separate instance of yargs to first parse the config (without the --help in the way) so we can parse the configFile first regardless
const configFile = [yargs.argv.config, './just.config.js', './just-task.js', './just.config.ts'].reduce(
(value, entry) => value || resolve(entry)
);
mark('registry:configModule');
if (configFile && fs.existsSync(configFile)) {
const ext = path.extname(configFile);
if (ext === '.ts' || ext === '.tsx') {
// TODO: add option to do typechecking as well
enableTypeScript({ transpileOnly: true });
}
try {
const configModule = require(configFile);
if (typeof configModule === 'function') {
configModule();
}
} catch (e) {
logger.error(`Invalid configuration file: ${configFile}`);
logger.error(`Error: ${e.stack || e.message || e}`);
process.exit(1);
}
} else {
logger.error(
`Cannot find config file "${configFile}".`,
`Please create a file called "just.config.js" in the root of the project next to "package.json".`
);
}
logger.perf('registry:configModule');
if (!validateCommands(yargs)) {
process.exit(1);
}
if (!this.hasDefault) {
yargs.demandCommand(1, 'No default tasks are defined.').help();
}
}
public set<TTaskFunction>(taskName: string, fn: TTaskFunction): TTaskFunction {
super.set(taskName, fn);
if (taskName === 'default') {
this.hasDefault = true;
}
return fn;
}
}
function validateCommands(yargs: any) {
const commandKeys = yargs.getCommandInstance().getCommands();
const argv = yargs.argv;
const unknown: string[] = [];
const currentContext = yargs.getContext();
if (commandKeys.length > 0) {
argv._.slice(currentContext.commands.length).forEach((key: string) => {
if (commandKeys.indexOf(key) === -1) {
unknown.push(key);
}
});
}
if (unknown.length > 0) {
logger.error(`Unknown command: ${unknown.join(', ')}`);
const recommended = recommendCommands(unknown[0], commandKeys);
if (recommended) {
logger.info(`Did you mean this task name: ${recommended}?`);
}
return false;
}
return true;
}
function recommendCommands(cmd: string, potentialCommands: string[]) {
const distance = require('yargs/lib/levenshtein');
const threshold = 3; // if it takes more than three edits, let's move on.
potentialCommands = potentialCommands.sort((a, b) => b.length - a.length);
let recommended = null;
let bestDistance = Infinity;
for (let i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) {
const d = distance(cmd, candidate);
if (d <= threshold && d < bestDistance) {
bestDistance = d;
recommended = candidate;
}
}
return recommended;
}

Просмотреть файл

@ -1,2 +0,0 @@
import * as yargs from './yargs/yargs';
export default yargs;

Просмотреть файл

@ -1,21 +0,0 @@
const yargs = () => yargs;
yargs.argv = {
config: undefined
} as { config: string | undefined };
yargs.command = () => yargs;
yargs.demandCommand = () => yargs;
yargs.help = () => yargs;
yargs.getCommandInstance = () => ({
getCommands: () => []
});
yargs.getContext = () => ({
commands: []
});
export = yargs;

Просмотреть файл

@ -1,14 +1,8 @@
import mockfs from 'mock-fs';
import path from 'path';
import yargsMock from './__mocks__/yargs/yargs';
import {
_isFileNameLike,
_tryResolve,
resetResolvePaths,
resolveCwd,
addResolvePath,
resolve
} from '../resolve';
import { _isFileNameLike, _tryResolve, resetResolvePaths, resolveCwd, addResolvePath, resolve } from '../resolve';
import * as option from '../option';
describe('_isFileNameLike', () => {
it('returns false for empty input', () => {
@ -83,6 +77,10 @@ describe('_tryResolve', () => {
});
describe('resolveCwd', () => {
beforeEach(() => {
jest.spyOn(option, 'argv').mockImplementation(() => ({ config: undefined } as any));
});
afterEach(() => {
mockfs.restore();
resetResolvePaths();
@ -117,9 +115,10 @@ describe('resolveCwd', () => {
});
describe('resolve', () => {
jest.spyOn(option, 'argv').mockImplementation(() => ({ config: undefined } as any));
afterEach(() => {
mockfs.restore();
yargsMock.argv.config = undefined;
resetResolvePaths();
});
@ -145,7 +144,9 @@ describe('resolve', () => {
mockfs({
a: { 'b.txt': '' }
});
yargsMock.argv.config = 'a/just-task.js';
jest.spyOn(option, 'argv').mockImplementation(() => ({ config: 'a/just-task.js' } as any));
expect(resolve('b.txt')).toContain(path.join('a', 'b.txt'));
});
@ -166,7 +167,9 @@ describe('resolve', () => {
d: { 'b.txt': '' }, // wrong
'b.txt': '' // wrong
});
yargsMock.argv.config = 'a/just-task.js';
jest.spyOn(option, 'argv').mockImplementation(() => ({ config: 'a/just-task.js' } as any));
addResolvePath('c');
expect(resolve('b.txt', 'd')).toContain(path.join('d', 'b.txt'));
});

Просмотреть файл

@ -1,22 +1,24 @@
import { task } from '../task';
import { parallel, undertaker } from '../undertaker';
import { JustTaskRegistry } from '../JustTaskRegistry';
import UndertakerRegistry from 'undertaker-registry';
import { logger } from '../logger';
import yargsMock from './__mocks__/yargs';
import path from 'path';
import * as option from '../option';
describe('task', () => {
beforeAll(() => {
yargsMock.argv.config = path.resolve(__dirname, '__mocks__/just-task.js');
jest.spyOn(option, 'argv').mockImplementation(() => ({ config: path.resolve(__dirname, '__mocks__/just-task.js') } as any));
jest.spyOn(logger, 'info').mockImplementation(() => undefined);
});
beforeEach(() => {
undertaker.registry(new JustTaskRegistry());
undertaker.registry(new UndertakerRegistry());
});
afterAll(() => {
yargsMock.argv.config = undefined;
jest.spyOn(option, 'argv').mockImplementation(() => ({ config: 'a/just-task.js' } as any));
jest.restoreAllMocks();
});

Просмотреть файл

@ -1,6 +1,8 @@
import { undertaker } from './undertaker';
import { JustTaskRegistry } from './JustTaskRegistry';
import yargs from 'yargs';
import { option, parseCommand } from './option';
import { logger } from 'just-task-logger';
import { TaskFunction } from './interfaces';
import { readConfig } from './config';
const originalEmitWarning = process.emitWarning;
@ -17,15 +19,33 @@ const originalEmitWarning = process.emitWarning;
return originalEmitWarning.apply(this, arguments);
};
yargs
.option({ config: { describe: 'path to a just-task.js file (includes the file name)' } })
.usage('$0 <cmd> [options]')
.updateStrings({
'Commands:': 'Tasks:\n'
});
function showHelp() {
const tasks = undertaker.registry().tasks();
const registry = new JustTaskRegistry();
console.log('All the tasks that are available to just:');
undertaker.registry(registry);
for (const [name, wrappedTask] of Object.entries(tasks)) {
const unwrapped = (wrappedTask as any).unwrap ? (wrappedTask as any).unwrap() : (wrappedTask as TaskFunction);
const description = (unwrapped as TaskFunction).description;
console.log(` ${name}${description ? `: ${description}` : ''}`);
}
}
yargs.parse();
// Define a built-in option of "config" so users can specify which path to choose for configurations
option('config', { describe: 'path to a just configuration file (includes the file name, e.g. /path/to/just.config.ts)' });
readConfig();
const registry = undertaker.registry();
const command = parseCommand();
if (command) {
if (registry.get(command)) {
undertaker.series(registry.get(command))(() => {});
} else {
logger.error(`Command not defined: ${command}`);
}
} else {
showHelp();
}

Просмотреть файл

@ -0,0 +1,46 @@
import fs from 'fs';
import path from 'path';
import { argv } from './option';
import { resolve } from './resolve';
import { mark, logger } from 'just-task-logger';
import { enableTypeScript } from './enableTypeScript';
export function readConfig() {
// uses a separate instance of yargs to first parse the config (without the --help in the way) so we can parse the configFile first regardless
let configFile: string | null = null;
for (const entry of [argv().config, './just.config.js', './just-task.js', './just.config.ts']) {
configFile = resolve(entry);
if (configFile) {
break;
}
}
mark('registry:configModule');
if (configFile && fs.existsSync(configFile)) {
const ext = path.extname(configFile);
if (ext === '.ts' || ext === '.tsx') {
// TODO: add option to do typechecking as well
enableTypeScript({ transpileOnly: true });
}
try {
const configModule = require(configFile);
if (typeof configModule === 'function') {
configModule();
}
} catch (e) {
logger.error(`Invalid configuration file: ${configFile}`);
logger.error(`Error: ${e.stack || e.message || e}`);
process.exit(1);
}
} else {
logger.error(
`Cannot find config file "${configFile}".`,
`Please create a file called "just.config.js" in the root of the project next to "package.json".`
);
}
logger.perf('registry:configModule');
}

Просмотреть файл

@ -13,4 +13,5 @@ export interface TaskContext {
export interface TaskFunction extends Undertaker.TaskFunctionParams {
(this: TaskContext, done: (error?: any) => void): void | Duplex | NodeJS.Process | Promise<never> | any;
cached?: () => void;
description?: string;
}

Просмотреть файл

@ -1,9 +1,90 @@
import yargs from 'yargs';
import parser, { Options, Arguments } from 'yargs-parser';
export function option(key: string, options: yargs.Options = {}): yargs.Argv {
return yargs.option.apply(yargs, [key, options]);
export interface OptionConfig {
/** Aliases for the argument, can be a string or array */
alias?: string | string[];
/** Argument should be an array */
array?: boolean;
/** Argument should be parsed as booleans: `{ boolean: ['x', 'y'] }`. */
boolean?: boolean;
/**
* Provide a custom synchronous function that returns a coerced value from the argument provided (or throws an error), e.g.
* `{ coerce: function (arg) { return modifiedArg } }`.
*/
coerce?: (arg: any) => any;
/** Indicate a key that should be used as a counter, e.g., `-vvv = {v: 3}`. */
count?: boolean;
/** Provide default values for keys: `{ default: { x: 33, y: 'hello world!' } }`. */
default?: { [key: string]: any };
/** Specify that a key requires n arguments: `{ narg: {x: 2} }`. */
narg?: number;
/** `path.normalize()` will be applied to values set to this key. */
normalize?: boolean;
/** Keys should be treated as strings (even if they resemble a number `-x 33`). */
string?: boolean;
/** Keys should be treated as numbers. */
number?: boolean;
/** A description of the option */
describe?: string;
}
export function argv(): yargs.Arguments {
return yargs.argv;
let argOptions: Options = {};
const descriptions: { [key: string]: string } = {};
const processArgs = process.argv.slice(2);
export function option(key: string, options: OptionConfig = {}) {
const booleanArgs = ['array', 'boolean', 'count', 'normalize', 'string', 'number'] as const;
const assignArgs = ['alias', 'coerce', 'default'] as const;
for (const argName of booleanArgs) {
if (options[argName]) {
if (!argOptions[argName]) {
argOptions[argName] = [];
}
const argOpts = argOptions[argName]! as string[];
if (argOpts.indexOf(key) === -1) {
argOpts.push(key);
}
}
}
for (const argName of assignArgs) {
if (options[argName]) {
if (!argOptions[argName]) {
argOptions[argName] = {};
}
argOptions[argName]![key] = options[argName];
}
}
if (options.describe) {
descriptions[key] = options.describe;
}
}
export function argv(): Arguments {
return parser(processArgs, argOptions);
}
export function parseCommand(): string | null {
const positionals = argv()._;
if (positionals.length > 0) {
return positionals[0];
}
return null;
}

Просмотреть файл

@ -1,12 +1,6 @@
import { sync as resolveSync } from 'resolve';
import path from 'path';
// It is important to keep this line like this:
// 1. it cannot be an import because TS will try to do type checks which isn't available in @types/yargs
// 2. this breaks a require.cache, which is needed because we need a new instance of yargs to check the config
// - this is because of the timing of when tasks are defined vs when this resolve is called the first time
// to figure out config path)
const yargsFn = require('yargs/yargs');
import { argv } from './option';
let resolvePaths: string[] = [__dirname];
@ -62,7 +56,8 @@ export function resolve(moduleName: string, cwd?: string): string | null {
if (!cwd) {
cwd = process.cwd();
}
const configArg = yargsFn(process.argv.slice(1).filter(a => a !== '--help')).argv.config;
const configArg = argv().config;
const configFilePath = configArg ? path.resolve(path.dirname(configArg)) : undefined;
const allResolvePaths = [cwd, ...(configFilePath ? [configFilePath] : []), ...resolvePaths];

Просмотреть файл

@ -1,9 +1,7 @@
import yargs from 'yargs';
import { undertaker } from './undertaker';
import { wrapTask } from './wrapTask';
import { TaskFunction } from './interfaces';
import { logger } from 'just-task-logger';
import { registerCachedTask, isCached, saveCache } from './cache';
import { registerCachedTask } from './cache';
export function task(firstParam: string | TaskFunction, secondParam?: string | TaskFunction, thirdParam?: TaskFunction): TaskFunction {
const argCount = arguments.length;
@ -19,7 +17,6 @@ export function task(firstParam: string | TaskFunction, secondParam?: string | T
};
undertaker.task(firstParam, wrapped);
yargs.command(getCommandModule(firstParam, ''));
return wrapped;
} else if (argCount === 2 && isString(firstParam) && isTaskFunction(secondParam)) {
@ -31,7 +28,6 @@ export function task(firstParam: string | TaskFunction, secondParam?: string | T
};
undertaker.task(firstParam, wrapped);
yargs.command(getCommandModule(firstParam, ''));
return wrapped;
} else if (argCount === 3 && isString(firstParam) && isString(secondParam) && isTaskFunction(thirdParam)) {
@ -41,8 +37,9 @@ export function task(firstParam: string | TaskFunction, secondParam?: string | T
registerCachedTask(firstParam);
};
wrapped.description = secondParam;
undertaker.task(firstParam, wrapped);
yargs.command(getCommandModule(firstParam, secondParam));
return wrapped;
} else {
@ -57,21 +54,3 @@ function isString(param: string | TaskFunction | undefined): param is string {
function isTaskFunction(param: string | TaskFunction | undefined): param is TaskFunction {
return typeof param === 'function';
}
function getCommandModule(taskName: string, describe?: string): yargs.CommandModule {
return {
command: taskName,
describe,
...(taskName === 'default' ? { aliases: ['*'] } : {}),
handler(_argvParam: any) {
if (isCached(taskName)) {
logger.info(`Skipped ${taskName} since it was cached`);
return;
}
return undertaker.parallel(taskName)(() => {
saveCache(taskName);
});
}
};
}

Просмотреть файл

@ -3,5 +3,5 @@
"compilerOptions": {
"outDir": "lib"
},
"include": ["src"]
"include": ["src", "types"]
}

Просмотреть файл

@ -2328,6 +2328,11 @@
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0"
integrity sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==
"@types/yargs-parser@^15.0.0":
version "15.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==
"@types/yargs@12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.1.tgz#c5ce4ad64499010ae4dc2acd9b14d49749a44233"
@ -3248,7 +3253,7 @@ babylon@^6.17.4:
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
bach@^1.0.0:
bach@^1.0.0, bach@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"
integrity sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=
@ -13364,7 +13369,7 @@ undertaker-registry@^1.0.0, undertaker-registry@^1.0.1:
resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50"
integrity sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=
undertaker@^1.2.0:
undertaker@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/undertaker/-/undertaker-1.2.1.tgz#701662ff8ce358715324dfd492a4f036055dfe4b"
integrity sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==
@ -14045,6 +14050,14 @@ yargs-parser@^13.1.0, yargs-parser@^13.1.1:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^18.1.2:
version "18.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1"
integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"