827 lines
21 KiB
TypeScript
827 lines
21 KiB
TypeScript
import type { Arguments } from 'yargs-parser';
|
|
import { TargetConfiguration, WorkspaceJsonConfiguration } from './workspace';
|
|
import { logger } from './logger';
|
|
|
|
type PropertyDescription = {
|
|
type?: string;
|
|
required?: string[];
|
|
enum?: string[];
|
|
properties?: any;
|
|
oneOf?: PropertyDescription[];
|
|
anyOf?: PropertyDescription[];
|
|
allOf?: PropertyDescription[];
|
|
items?: any;
|
|
alias?: string;
|
|
aliases?: string[];
|
|
description?: string;
|
|
format?: string;
|
|
visible?: boolean;
|
|
default?:
|
|
| string
|
|
| number
|
|
| boolean
|
|
| string[]
|
|
| { [key: string]: string | number | boolean | string[] };
|
|
$ref?: string;
|
|
$default?: { $source: 'argv'; index: number } | { $source: 'projectName' };
|
|
additionalProperties?: boolean;
|
|
'x-prompt'?:
|
|
| string
|
|
| { message: string; type: string; items: any[]; multiselect?: boolean };
|
|
'x-deprecated'?: boolean | string;
|
|
|
|
// Numbers Only
|
|
multipleOf?: number;
|
|
minimum?: number;
|
|
exclusiveMinimum?: number;
|
|
maximum?: number;
|
|
exclusiveMaximum?: number;
|
|
|
|
// Strings Only
|
|
pattern?: string;
|
|
minLength?: number;
|
|
maxLength?: number;
|
|
};
|
|
|
|
type Properties = {
|
|
[p: string]: PropertyDescription;
|
|
};
|
|
export type Schema = {
|
|
properties: Properties;
|
|
required?: string[];
|
|
description?: string;
|
|
definitions?: Properties;
|
|
additionalProperties?: boolean;
|
|
};
|
|
|
|
export type Unmatched = {
|
|
name: string;
|
|
possible: string[];
|
|
};
|
|
|
|
export type Options = {
|
|
'--'?: Unmatched[];
|
|
[k: string]: string | number | boolean | string[] | Unmatched[];
|
|
};
|
|
|
|
export async function handleErrors(isVerbose: boolean, fn: Function) {
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
if (err.constructor.name === 'UnsuccessfulWorkflowExecution') {
|
|
logger.error('The generator workflow failed. See above.');
|
|
} else if (err.message) {
|
|
logger.error(err.message);
|
|
} else {
|
|
logger.error(err);
|
|
}
|
|
if (isVerbose && err.stack) {
|
|
logger.info(err.stack);
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
function camelCase(input: string): string {
|
|
if (input.indexOf('-') > 1) {
|
|
return input
|
|
.toLowerCase()
|
|
.replace(/-(.)/g, (match, group1) => group1.toUpperCase());
|
|
} else {
|
|
return input;
|
|
}
|
|
}
|
|
|
|
export function convertToCamelCase(parsed: Arguments): Options {
|
|
return Object.keys(parsed).reduce(
|
|
(m, c) => ({ ...m, [camelCase(c)]: parsed[c] }),
|
|
{}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Coerces (and replaces) options identified as 'boolean' or 'number' in the Schema
|
|
*
|
|
* @param opts The options to check
|
|
* @param schema The schema definition with types to check against
|
|
*
|
|
*/
|
|
export function coerceTypesInOptions(opts: Options, schema: Schema): Options {
|
|
Object.keys(opts).forEach((k) => {
|
|
const prop = findSchemaForProperty(k, schema);
|
|
|
|
opts[k] = coerceType(prop?.description, opts[k]);
|
|
});
|
|
return opts;
|
|
}
|
|
|
|
function coerceType(prop: PropertyDescription | undefined, value: any) {
|
|
if (!prop) return value;
|
|
if (typeof value !== 'string' && value !== undefined) return value;
|
|
|
|
if (prop.oneOf) {
|
|
for (let i = 0; i < prop.oneOf.length; ++i) {
|
|
const coerced = coerceType(prop.oneOf[i], value);
|
|
if (coerced !== value) {
|
|
return coerced;
|
|
}
|
|
}
|
|
return value;
|
|
} else if (
|
|
normalizedPrimitiveType(prop.type) == 'boolean' &&
|
|
isConvertibleToBoolean(value)
|
|
) {
|
|
return value === true || value == 'true';
|
|
} else if (
|
|
normalizedPrimitiveType(prop.type) == 'number' &&
|
|
isConvertibleToNumber(value)
|
|
) {
|
|
return Number(value);
|
|
} else if (prop.type == 'array') {
|
|
return value.split(',').map((v) => coerceType(prop.items, v));
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts any options passed in with short aliases to their full names if found
|
|
* Unmatched options are added to opts['--']
|
|
*
|
|
* @param opts The options passed in by the user
|
|
* @param schema The schema definition to check against
|
|
*/
|
|
export function convertAliases(
|
|
opts: Options,
|
|
schema: Schema,
|
|
excludeUnmatched: boolean
|
|
): Options {
|
|
return Object.keys(opts).reduce((acc, k) => {
|
|
const prop = findSchemaForProperty(k, schema);
|
|
if (prop) {
|
|
acc[prop.name] = opts[k];
|
|
} else if (excludeUnmatched) {
|
|
if (!acc['--']) {
|
|
acc['--'] = [];
|
|
}
|
|
acc['--'].push({
|
|
name: k,
|
|
possible: [],
|
|
});
|
|
} else {
|
|
acc[k] = opts[k];
|
|
}
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
export class SchemaError {
|
|
constructor(public readonly message: string) {}
|
|
}
|
|
|
|
export function validateOptsAgainstSchema(
|
|
opts: { [k: string]: any },
|
|
schema: Schema
|
|
) {
|
|
validateObject(
|
|
opts,
|
|
schema.properties || {},
|
|
schema.required || [],
|
|
schema.additionalProperties,
|
|
schema.definitions || {}
|
|
);
|
|
}
|
|
|
|
export function validateObject(
|
|
opts: { [k: string]: any },
|
|
properties: Properties,
|
|
required: string[],
|
|
additionalProperties: boolean | undefined,
|
|
definitions: Properties
|
|
) {
|
|
required.forEach((p) => {
|
|
if (opts[p] === undefined) {
|
|
throw new SchemaError(`Required property '${p}' is missing`);
|
|
}
|
|
});
|
|
|
|
if (additionalProperties === false) {
|
|
Object.keys(opts).find((p) => {
|
|
if (Object.keys(properties).indexOf(p) === -1) {
|
|
throw new SchemaError(`'${p}' is not found in schema`);
|
|
}
|
|
});
|
|
}
|
|
|
|
Object.keys(opts).forEach((p) => {
|
|
validateProperty(p, opts[p], properties[p], definitions);
|
|
});
|
|
}
|
|
|
|
function validateProperty(
|
|
propName: string,
|
|
value: any,
|
|
schema: PropertyDescription,
|
|
definitions: Properties
|
|
) {
|
|
if (!schema) return;
|
|
|
|
if (schema.$ref) {
|
|
schema = resolveDefinition(schema.$ref, definitions);
|
|
}
|
|
|
|
if (schema.oneOf) {
|
|
if (!Array.isArray(schema.oneOf))
|
|
throw new Error(`Invalid schema file. oneOf must be an array.`);
|
|
|
|
const passes =
|
|
schema.oneOf.filter((r) => {
|
|
try {
|
|
const rule = { type: schema.type, ...r };
|
|
validateProperty(propName, value, rule, definitions);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}).length === 1;
|
|
if (!passes) throwInvalidSchema(propName, schema);
|
|
return;
|
|
}
|
|
|
|
if (schema.anyOf) {
|
|
if (!Array.isArray(schema.anyOf))
|
|
throw new Error(`Invalid schema file. anyOf must be an array.`);
|
|
|
|
let passes = false;
|
|
schema.anyOf.forEach((r) => {
|
|
try {
|
|
const rule = { type: schema.type, ...r };
|
|
validateProperty(propName, value, rule, definitions);
|
|
passes = true;
|
|
} catch (e) {}
|
|
});
|
|
if (!passes) throwInvalidSchema(propName, schema);
|
|
return;
|
|
}
|
|
|
|
if (schema.allOf) {
|
|
if (!Array.isArray(schema.allOf))
|
|
throw new Error(`Invalid schema file. anyOf must be an array.`);
|
|
|
|
if (
|
|
!schema.allOf.every((r) => {
|
|
try {
|
|
const rule = { type: schema.type, ...r };
|
|
validateProperty(propName, value, rule, definitions);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
})
|
|
) {
|
|
throwInvalidSchema(propName, schema);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const isPrimitive = typeof value !== 'object';
|
|
if (isPrimitive) {
|
|
if (schema.type && typeof value !== normalizedPrimitiveType(schema.type)) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. '${value}' should be a '${schema.type}'.`
|
|
);
|
|
}
|
|
|
|
if (schema.enum && !schema.enum.includes(value)) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. '${value}' should be one of ${schema.enum.join(
|
|
','
|
|
)}.`
|
|
);
|
|
}
|
|
|
|
if (schema.type === 'number') {
|
|
if (
|
|
typeof schema.multipleOf === 'number' &&
|
|
value % schema.multipleOf !== 0
|
|
) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. ${value} should be a multiple of ${schema.multipleOf}.`
|
|
);
|
|
}
|
|
if (typeof schema.minimum === 'number' && value < schema.minimum) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. ${value} should be at least ${schema.minimum}`
|
|
);
|
|
}
|
|
if (
|
|
typeof schema.exclusiveMinimum === 'number' &&
|
|
value <= schema.exclusiveMinimum
|
|
) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. ${value} should be greater than ${schema.exclusiveMinimum}`
|
|
);
|
|
}
|
|
if (typeof schema.maximum === 'number' && value > schema.maximum) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. ${value} should be at most ${schema.maximum}`
|
|
);
|
|
}
|
|
if (
|
|
typeof schema.exclusiveMaximum === 'number' &&
|
|
value >= schema.exclusiveMaximum
|
|
) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. ${value} should be less than ${schema.exclusiveMaximum}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (schema.type === 'string') {
|
|
if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. '${value}' should match the pattern '${schema.pattern}'.`
|
|
);
|
|
}
|
|
|
|
if (
|
|
typeof schema.minLength === 'number' &&
|
|
value.length < schema.minLength
|
|
) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. '${value}' (${value.length} character(s)) should have at least ${schema.minLength} character(s).`
|
|
);
|
|
}
|
|
|
|
if (
|
|
typeof schema.maxLength === 'number' &&
|
|
value.length > schema.maxLength
|
|
) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema. '${value}' (${value.length} character(s)) should have at most ${schema.maxLength} character(s).`
|
|
);
|
|
}
|
|
}
|
|
} else if (Array.isArray(value)) {
|
|
if (schema.type !== 'array') throwInvalidSchema(propName, schema);
|
|
value.forEach((valueInArray) =>
|
|
validateProperty(propName, valueInArray, schema.items || {}, definitions)
|
|
);
|
|
} else {
|
|
if (schema.type !== 'object') throwInvalidSchema(propName, schema);
|
|
validateObject(
|
|
value,
|
|
schema.properties || {},
|
|
schema.required || [],
|
|
schema.additionalProperties,
|
|
definitions
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unfortunately, due to use supporting Angular Devkit, we have to do the following
|
|
* conversions.
|
|
*/
|
|
function normalizedPrimitiveType(type: string) {
|
|
if (type === 'integer') return 'number';
|
|
return type;
|
|
}
|
|
|
|
function throwInvalidSchema(propName: string, schema: any) {
|
|
throw new SchemaError(
|
|
`Property '${propName}' does not match the schema.\n${JSON.stringify(
|
|
schema,
|
|
null,
|
|
2
|
|
)}'`
|
|
);
|
|
}
|
|
|
|
export function setDefaults(opts: { [k: string]: any }, schema: Schema) {
|
|
setDefaultsInObject(opts, schema.properties || {}, schema.definitions || {});
|
|
return opts;
|
|
}
|
|
|
|
function setDefaultsInObject(
|
|
opts: { [k: string]: any },
|
|
properties: Properties,
|
|
definitions: Properties
|
|
) {
|
|
Object.keys(properties).forEach((p) => {
|
|
setPropertyDefault(opts, p, properties[p], definitions);
|
|
});
|
|
}
|
|
|
|
function setPropertyDefault(
|
|
opts: { [k: string]: any },
|
|
propName: string,
|
|
schema: any,
|
|
definitions: Properties
|
|
) {
|
|
if (schema.$ref) {
|
|
schema = resolveDefinition(schema.$ref, definitions);
|
|
}
|
|
|
|
if (schema.type !== 'object' && schema.type !== 'array') {
|
|
if (opts[propName] === undefined && schema.default !== undefined) {
|
|
opts[propName] = schema.default;
|
|
}
|
|
} else if (schema.type === 'array') {
|
|
const items = schema.items || {};
|
|
if (
|
|
opts[propName] &&
|
|
Array.isArray(opts[propName]) &&
|
|
items.type === 'object'
|
|
) {
|
|
opts[propName].forEach((valueInArray) =>
|
|
setDefaultsInObject(valueInArray, items.properties || {}, definitions)
|
|
);
|
|
} else if (!opts[propName] && schema.default) {
|
|
opts[propName] = schema.default;
|
|
}
|
|
} else {
|
|
const wasUndefined = opts[propName] === undefined;
|
|
if (wasUndefined) {
|
|
// We need an object to set values onto
|
|
opts[propName] = {};
|
|
}
|
|
|
|
setDefaultsInObject(opts[propName], schema.properties || {}, definitions);
|
|
|
|
// If the property was initially undefined but no properties were added, we remove it again instead of having an {}
|
|
if (wasUndefined && Object.keys(opts[propName]).length === 0) {
|
|
delete opts[propName];
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveDefinition(ref: string, definitions: Properties) {
|
|
if (!ref.startsWith('#/definitions/')) {
|
|
throw new Error(`$ref should start with "#/definitions/"`);
|
|
}
|
|
const definition = ref.split('#/definitions/')[1];
|
|
if (!definitions[definition]) {
|
|
throw new Error(`Cannot resolve ${ref}`);
|
|
}
|
|
return definitions[definition];
|
|
}
|
|
|
|
export function applyVerbosity(
|
|
options: Record<string, unknown>,
|
|
schema: Schema,
|
|
isVerbose: boolean
|
|
) {
|
|
if (
|
|
(schema.additionalProperties || 'verbose' in schema.properties) &&
|
|
isVerbose
|
|
) {
|
|
options['verbose'] = true;
|
|
}
|
|
}
|
|
|
|
export function combineOptionsForExecutor(
|
|
commandLineOpts: Options,
|
|
config: string,
|
|
target: TargetConfiguration,
|
|
schema: Schema,
|
|
defaultProjectName: string | null,
|
|
relativeCwd: string | null,
|
|
isVerbose = false
|
|
) {
|
|
const r = convertAliases(
|
|
coerceTypesInOptions(commandLineOpts, schema),
|
|
schema,
|
|
false
|
|
);
|
|
let combined = target.options || {};
|
|
if (config && target.configurations && target.configurations[config]) {
|
|
Object.assign(combined, target.configurations[config]);
|
|
}
|
|
combined = convertAliases(combined, schema, false);
|
|
Object.assign(combined, r);
|
|
convertSmartDefaultsIntoNamedParams(
|
|
combined,
|
|
schema,
|
|
(commandLineOpts['_'] as string[]) || [],
|
|
defaultProjectName,
|
|
relativeCwd
|
|
);
|
|
warnDeprecations(combined, schema);
|
|
setDefaults(combined, schema);
|
|
validateOptsAgainstSchema(combined, schema);
|
|
applyVerbosity(combined, schema, isVerbose);
|
|
return combined;
|
|
}
|
|
|
|
export async function combineOptionsForGenerator(
|
|
commandLineOpts: Options,
|
|
collectionName: string,
|
|
generatorName: string,
|
|
wc: WorkspaceJsonConfiguration | null,
|
|
schema: Schema,
|
|
isInteractive: boolean,
|
|
defaultProjectName: string | null,
|
|
relativeCwd: string | null,
|
|
isVerbose = false
|
|
) {
|
|
const generatorDefaults = wc
|
|
? getGeneratorDefaults(
|
|
defaultProjectName,
|
|
wc,
|
|
collectionName,
|
|
generatorName
|
|
)
|
|
: {};
|
|
let combined = convertAliases(
|
|
coerceTypesInOptions({ ...generatorDefaults, ...commandLineOpts }, schema),
|
|
schema,
|
|
false
|
|
);
|
|
convertSmartDefaultsIntoNamedParams(
|
|
combined,
|
|
schema,
|
|
(commandLineOpts['_'] as string[]) || [],
|
|
defaultProjectName,
|
|
relativeCwd
|
|
);
|
|
|
|
if (isInteractive && isTTY()) {
|
|
combined = await promptForValues(combined, schema);
|
|
}
|
|
|
|
warnDeprecations(combined, schema);
|
|
setDefaults(combined, schema);
|
|
|
|
validateOptsAgainstSchema(combined, schema);
|
|
applyVerbosity(combined, schema, isVerbose);
|
|
return combined;
|
|
}
|
|
|
|
export function warnDeprecations(
|
|
opts: { [k: string]: any },
|
|
schema: Schema
|
|
): void {
|
|
Object.keys(opts).forEach((option) => {
|
|
const deprecated = schema.properties[option]?.['x-deprecated'];
|
|
if (deprecated) {
|
|
logger.warn(
|
|
`Option "${option}" is deprecated${
|
|
typeof deprecated == 'string' ? ': ' + deprecated : '.'
|
|
}`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function convertSmartDefaultsIntoNamedParams(
|
|
opts: { [k: string]: any },
|
|
schema: Schema,
|
|
argv: string[],
|
|
defaultProjectName: string | null,
|
|
relativeCwd: string | null
|
|
) {
|
|
Object.entries(schema.properties).forEach(([k, v]) => {
|
|
if (
|
|
opts[k] === undefined &&
|
|
v.$default !== undefined &&
|
|
v.$default.$source === 'argv' &&
|
|
argv[v.$default.index]
|
|
) {
|
|
opts[k] = coerceType(v, argv[v.$default.index]);
|
|
} else if (
|
|
opts[k] === undefined &&
|
|
v.$default !== undefined &&
|
|
v.$default.$source === 'projectName' &&
|
|
defaultProjectName
|
|
) {
|
|
opts[k] = defaultProjectName;
|
|
} else if (
|
|
opts[k] === undefined &&
|
|
v.format === 'path' &&
|
|
v.visible === false &&
|
|
relativeCwd
|
|
) {
|
|
opts[k] = relativeCwd.replace(/\\/g, '/');
|
|
}
|
|
});
|
|
delete opts['_'];
|
|
}
|
|
|
|
function getGeneratorDefaults(
|
|
projectName: string | null,
|
|
wc: WorkspaceJsonConfiguration,
|
|
collectionName: string,
|
|
generatorName: string
|
|
) {
|
|
let defaults = {};
|
|
if (wc.generators) {
|
|
if (
|
|
wc.generators[collectionName] &&
|
|
wc.generators[collectionName][generatorName]
|
|
) {
|
|
defaults = {
|
|
...defaults,
|
|
...wc.generators[collectionName][generatorName],
|
|
};
|
|
}
|
|
if (wc.generators[`${collectionName}:${generatorName}`]) {
|
|
defaults = {
|
|
...defaults,
|
|
...wc.generators[`${collectionName}:${generatorName}`],
|
|
};
|
|
}
|
|
}
|
|
if (
|
|
projectName &&
|
|
wc.projects[projectName] &&
|
|
wc.projects[projectName].generators
|
|
) {
|
|
const g = wc.projects[projectName].generators;
|
|
if (g[collectionName] && g[collectionName][generatorName]) {
|
|
defaults = { ...defaults, ...g[collectionName][generatorName] };
|
|
}
|
|
if (g[`${collectionName}:${generatorName}`]) {
|
|
defaults = {
|
|
...defaults,
|
|
...g[`${collectionName}:${generatorName}`],
|
|
};
|
|
}
|
|
}
|
|
return defaults;
|
|
}
|
|
|
|
async function promptForValues(opts: Options, schema: Schema) {
|
|
interface Prompt {
|
|
name: string;
|
|
type: 'input' | 'select' | 'multiselect' | 'confirm' | 'numeral';
|
|
message: string;
|
|
initial?: any;
|
|
choices?: (string | { name: string; message: string })[];
|
|
}
|
|
|
|
const prompts: Prompt[] = [];
|
|
Object.entries(schema.properties).forEach(([k, v]) => {
|
|
if (v['x-prompt'] && opts[k] === undefined) {
|
|
const question: Prompt = {
|
|
name: k,
|
|
} as any;
|
|
|
|
if (v.default) {
|
|
question.initial = v.default;
|
|
}
|
|
|
|
if (typeof v['x-prompt'] === 'string') {
|
|
question.message = v['x-prompt'];
|
|
if (v.type === 'string' && v.enum && Array.isArray(v.enum)) {
|
|
question.type = 'select';
|
|
question.choices = [...v.enum];
|
|
} else {
|
|
question.type = v.type === 'boolean' ? 'confirm' : 'input';
|
|
}
|
|
} else if (v['x-prompt'].type == 'number') {
|
|
question.message = v['x-prompt'].message;
|
|
question.type = 'numeral';
|
|
} else if (
|
|
v['x-prompt'].type == 'confirmation' ||
|
|
v['x-prompt'].type == 'confirm'
|
|
) {
|
|
question.message = v['x-prompt'].message;
|
|
question.type = 'confirm';
|
|
} else {
|
|
question.message = v['x-prompt'].message;
|
|
question.type = v['x-prompt'].multiselect ? 'multiselect' : 'select';
|
|
question.choices =
|
|
v['x-prompt'].items &&
|
|
v['x-prompt'].items.map((item) => {
|
|
if (typeof item == 'string') {
|
|
return item;
|
|
} else {
|
|
return {
|
|
message: item.label,
|
|
name: item.value,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
prompts.push(question);
|
|
}
|
|
});
|
|
|
|
return await (
|
|
await import('enquirer')
|
|
)
|
|
.prompt(prompts)
|
|
.then((values) => ({ ...opts, ...values }))
|
|
.catch((e) => {
|
|
console.error(e);
|
|
process.exit(0);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Tries to find what the user meant by unmatched commands
|
|
*
|
|
* @param opts The options passed in by the user
|
|
* @param schema The schema definition to check against
|
|
*
|
|
*/
|
|
export function lookupUnmatched(opts: Options, schema: Schema): Options {
|
|
if (opts['--']) {
|
|
const props = Object.keys(schema.properties);
|
|
|
|
opts['--'].forEach((unmatched) => {
|
|
unmatched.possible = props.filter(
|
|
(p) => levenshtein(p, unmatched.name) < 3
|
|
);
|
|
});
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
function findSchemaForProperty(
|
|
propName: string,
|
|
schema: Schema
|
|
): { name: string; description: PropertyDescription } | null {
|
|
if (propName in schema.properties) {
|
|
return {
|
|
name: propName,
|
|
description: schema.properties[propName],
|
|
};
|
|
}
|
|
const found = Object.entries(schema.properties).find(
|
|
([_, d]) =>
|
|
d.alias === propName ||
|
|
(Array.isArray(d.aliases) && d.aliases.includes(propName))
|
|
);
|
|
if (found) {
|
|
const [name, description] = found;
|
|
return { name, description };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function levenshtein(a: string, b: string) {
|
|
if (a.length == 0) {
|
|
return b.length;
|
|
}
|
|
if (b.length == 0) {
|
|
return a.length;
|
|
}
|
|
const matrix = [];
|
|
|
|
for (let i = 0; i <= b.length; i++) {
|
|
matrix[i] = [i];
|
|
}
|
|
for (let j = 0; j <= a.length; j++) {
|
|
matrix[0][j] = j;
|
|
}
|
|
for (let i = 1; i <= b.length; i++) {
|
|
for (let j = 1; j <= a.length; j++) {
|
|
if (b.charAt(i - 1) == a.charAt(j - 1)) {
|
|
matrix[i][j] = matrix[i - 1][j - 1];
|
|
} else {
|
|
matrix[i][j] = Math.min(
|
|
matrix[i - 1][j - 1] + 1,
|
|
matrix[i][j - 1] + 1,
|
|
matrix[i - 1][j] + 1
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return matrix[b.length][a.length];
|
|
}
|
|
|
|
function isTTY(): boolean {
|
|
return !!process.stdout.isTTY && process.env['CI'] !== 'true';
|
|
}
|
|
|
|
/**
|
|
* Verifies whether the given value can be converted to a boolean
|
|
* @param value
|
|
*/
|
|
function isConvertibleToBoolean(value) {
|
|
if ('boolean' === typeof value) {
|
|
return true;
|
|
}
|
|
|
|
if ('string' === typeof value && /true|false/.test(value)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Verifies whether the given value can be converted to a number
|
|
* @param value
|
|
*/
|
|
function isConvertibleToNumber(value) {
|
|
// exclude booleans explicitly
|
|
if ('boolean' === typeof value) {
|
|
return false;
|
|
}
|
|
|
|
return !isNaN(+value);
|
|
}
|