UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

665 lines (664 loc) • 25.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SchemaError = void 0; exports.convertToCamelCase = convertToCamelCase; exports.coerceTypesInOptions = coerceTypesInOptions; exports.convertAliases = convertAliases; exports.validateOptsAgainstSchema = validateOptsAgainstSchema; exports.validateObject = validateObject; exports.setDefaults = setDefaults; exports.applyVerbosity = applyVerbosity; exports.combineOptionsForExecutor = combineOptionsForExecutor; exports.combineOptionsForGenerator = combineOptionsForGenerator; exports.warnDeprecations = warnDeprecations; exports.convertSmartDefaultsIntoNamedParams = convertSmartDefaultsIntoNamedParams; exports.getPromptsForSchema = getPromptsForSchema; const logger_1 = require("./logger"); function camelCase(input) { if (input.indexOf('-') > 1) { return input .toLowerCase() .replace(/-(.)/g, (match, group1) => group1.toUpperCase()); } else { return input; } } function convertToCamelCase(parsed, schema) { return Object.keys(parsed).reduce((m, c) => { if (schema.properties[camelCase(c)]) { return { ...m, [camelCase(c)]: parsed[c] }; } else { return { ...m, [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 * */ function coerceTypesInOptions(opts, schema) { Object.keys(opts).forEach((k) => { const prop = findSchemaForProperty(k, schema); opts[k] = coerceType(prop?.description, opts[k]); }); return opts; } function coerceType(prop, value) { 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 (Array.isArray(prop.type)) { for (let i = 0; i < prop.type.length; ++i) { const coerced = coerceType({ type: prop.type[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 */ function convertAliases(opts, schema, excludeUnmatched) { 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; }, {}); } class SchemaError { constructor(message) { this.message = message; } } exports.SchemaError = SchemaError; function validateOptsAgainstSchema(opts, schema) { validateObject(opts, schema, schema.definitions || {}); } function validateObject(opts, schema, definitions) { if (schema.anyOf) { const errors = []; for (const s of schema.anyOf) { try { validateObject(opts, s, definitions); } catch (e) { errors.push(e); } } if (errors.length === schema.anyOf.length) { throw new Error(`Options did not match schema. Please fix any of the following errors:\n${errors .map((e) => ' - ' + e.message) .join('\n')}`); } } if (schema.oneOf) { const matches = []; const errors = []; for (const propertyDescription of schema.oneOf) { try { validateObject(opts, propertyDescription, definitions); matches.push(propertyDescription); } catch (error) { errors.push(error); } } // If the options matched none of the oneOf property descriptions if (matches.length === 0) { throw new Error(`Options did not match schema: ${JSON.stringify(opts, null, 2)}.\nPlease fix 1 of the following errors:\n${errors .map((e) => ' - ' + e.message) .join('\n')}`); } // If the options matched none of the oneOf property descriptions if (matches.length > 1) { throw new Error(`Options did not match schema: ${JSON.stringify(opts, null, 2)}.\nShould only match one of \n${matches .map((m) => ' - ' + JSON.stringify(m)) .join('\n')}`); } } (schema.required ?? []).forEach((p) => { if (opts[p] === undefined) { throw new SchemaError(`Required property '${p}' is missing`); } }); if (schema.additionalProperties !== undefined && schema.additionalProperties !== true) { Object.keys(opts).find((p) => { if (Object.keys(schema.properties ?? {}).indexOf(p) === -1 && (!schema.patternProperties || !Object.keys(schema.patternProperties).some((pattern) => new RegExp(pattern).test(p)))) { if (p === '_') { throw new SchemaError(`Schema does not support positional arguments. Argument '${opts[p]}' found`); } else if (schema.additionalProperties === false) { throw new SchemaError(`'${p}' is not found in schema`); } else if (typeof schema.additionalProperties === 'object') { validateProperty(p, opts[p], schema.additionalProperties, definitions); } } }); } Object.keys(opts).forEach((p) => { validateProperty(p, opts[p], (schema.properties ?? {})[p], definitions); if (schema.patternProperties) { Object.keys(schema.patternProperties).forEach((pattern) => { if (new RegExp(pattern).test(p)) { validateProperty(p, opts[p], schema.patternProperties[pattern], definitions); } }); } }); } function validateProperty(propName, value, schema, definitions) { 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. allOf 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.const !== undefined && value !== schema.const) { throw new SchemaError(`Property '${propName}' does not match the schema. '${value}' should be '${schema.const}'.`); } if (Array.isArray(schema.type)) { const passes = schema.type.some((t) => { try { const rule = { type: t }; validateProperty(propName, value, rule, definitions); return true; } catch (e) { return false; } }); if (!passes) { throw new SchemaError(`Property '${propName}' does not match the schema. '${value}' should be a '${schema.type}'.`); } } else 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, definitions); } } /** * Unfortunately, due to use supporting Angular Devkit, we have to do the following * conversions. */ function normalizedPrimitiveType(type) { if (type === 'integer') return 'number'; return type; } function throwInvalidSchema(propName, schema) { throw new SchemaError(`Property '${propName}' does not match the schema.\n${JSON.stringify(schema, null, 2)}'`); } function setDefaults(opts, schema) { setDefaultsInObject(opts, schema.properties || {}, schema.definitions || {}); return opts; } function setDefaultsInObject(opts, properties, definitions) { Object.keys(properties).forEach((p) => { setPropertyDefault(opts, p, properties[p], definitions); }); } function setPropertyDefault(opts, propName, schema, definitions) { 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, definitions) { 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]; } function applyVerbosity(options, schema, isVerbose) { if ((schema.additionalProperties === true || 'verbose' in schema.properties) && isVerbose) { options['verbose'] = true; } } function combineOptionsForExecutor(commandLineOpts, config, target, schema, defaultProjectName, relativeCwd, isVerbose = false) { const r = convertAliases(coerceTypesInOptions(convertToCamelCase(commandLineOpts, schema), 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, defaultProjectName, relativeCwd); warnDeprecations(combined, schema); setDefaults(combined, schema); validateOptsAgainstSchema(combined, schema); applyVerbosity(combined, schema, isVerbose); return combined; } async function combineOptionsForGenerator(commandLineOpts, collectionName, generatorName, projectsConfigurations, nxJsonConfiguration, schema, isInteractive, defaultProjectName, relativeCwd, isVerbose = false) { const generatorDefaults = projectsConfigurations ? getGeneratorDefaults(defaultProjectName, projectsConfigurations, nxJsonConfiguration, collectionName, generatorName) : {}; let combined = convertAliases(coerceTypesInOptions({ ...generatorDefaults, ...commandLineOpts }, schema), schema, false); warnDeprecations(combined, schema); convertSmartDefaultsIntoNamedParams(combined, schema, defaultProjectName, relativeCwd); if (isInteractive && isTTY()) { combined = await promptForValues(combined, schema, projectsConfigurations); } setDefaults(combined, schema); validateOptsAgainstSchema(combined, schema); applyVerbosity(combined, schema, isVerbose); return combined; } function warnDeprecations(opts, schema) { Object.keys(opts).forEach((option) => { const deprecated = schema.properties[option]?.['x-deprecated']; if (deprecated) { logger_1.logger.warn(`Option "${option}" is deprecated${typeof deprecated == 'string' ? ': ' + deprecated : '.'}`); } }); } function convertSmartDefaultsIntoNamedParams(opts, schema, defaultProjectName, relativeCwd) { const argv = opts['_'] || []; const usedPositionalArgs = {}; Object.entries(schema.properties).forEach(([k, v]) => { if (opts[k] === undefined && v.$default !== undefined && v.$default.$source === 'argv' && argv[v.$default.index]) { usedPositionalArgs[v.$default.index] = true; opts[k] = coerceType(v, argv[v.$default.index]); } else if (v.$default !== undefined && v.$default.$source === 'unparsed') { opts[k] = opts['__overrides_unparsed__'] || []; } 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, '/'); } else if (opts[k] === undefined && v.$default !== undefined && v.$default.$source === 'workingDirectory' && relativeCwd) { opts[k] = relativeCwd.replace(/\\/g, '/'); } }); const leftOverPositionalArgs = []; for (let i = 0; i < argv.length; ++i) { if (!usedPositionalArgs[i]) { leftOverPositionalArgs.push(argv[i]); } } if (leftOverPositionalArgs.length === 0) { delete opts['_']; } else { opts['_'] = leftOverPositionalArgs; } delete opts['__overrides_unparsed__']; } function getGeneratorDefaults(projectName, projectsConfigurations, nxJsonConfiguration, collectionName, generatorName) { let defaults = {}; if (nxJsonConfiguration?.generators) { if (nxJsonConfiguration.generators[collectionName]?.[generatorName]) { defaults = { ...defaults, ...nxJsonConfiguration.generators[collectionName][generatorName], }; } if (nxJsonConfiguration.generators[`${collectionName}:${generatorName}`]) { defaults = { ...defaults, ...nxJsonConfiguration.generators[`${collectionName}:${generatorName}`], }; } } if (projectName && projectsConfigurations?.projects[projectName]?.generators) { const g = projectsConfigurations.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; } function getPromptsForSchema(opts, schema, projectsConfigurations) { const prompts = []; Object.entries(schema.properties).forEach(([k, v]) => { if (v['x-prompt'] && opts[k] === undefined) { const question = { name: k, }; if (v.default) { question.initial = v.default; } // Normalize x-prompt if (typeof v['x-prompt'] === 'string') { const message = v['x-prompt']; if (v.type === 'boolean') { v['x-prompt'] = { type: 'confirm', message, }; } else if (v.type === 'array' && v.items?.enum) { v['x-prompt'] = { type: 'multiselect', items: v.items.enum, message, }; } else { v['x-prompt'] = { type: 'input', message, }; } } question.message = v['x-prompt'].message; question.validate = (s) => { try { validateProperty(k, s, v, schema.definitions || {}); return true; } catch (e) { return e.message; } }; // Limit the number of choices displayed so that the prompt fits on the screen const limitForChoicesDisplayed = process.stdout.rows - question.message.split('\n').length; if (v.type === 'string' && v.enum && Array.isArray(v.enum)) { question.type = 'autocomplete'; question.choices = [...v.enum]; question.limit = limitForChoicesDisplayed; } else if (v.type === 'string' && (v.$default?.$source === 'projectName' || k === 'project' || k === 'projectName' || v['x-dropdown'] === 'projects') && projectsConfigurations) { question.type = 'autocomplete'; question.choices = Object.keys(projectsConfigurations.projects); question.limit = limitForChoicesDisplayed; } else if (v.type === 'number' || v['x-prompt'].type == 'number') { question.type = 'numeral'; } else if (v['x-prompt'].type == 'confirmation' || v['x-prompt'].type == 'confirm') { question.type = 'confirm'; } else if (v['x-prompt'].items) { question.type = v['x-prompt'].multiselect || v.type === 'array' ? 'multiselect' : 'autocomplete'; 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, }; } }); question.limit = limitForChoicesDisplayed; } else if (v.type === 'boolean') { question.type = 'confirm'; } else { question.type = 'input'; } prompts.push(question); } }); return prompts; } async function promptForValues(opts, schema, projectsConfigurations) { return await (await Promise.resolve().then(() => require('enquirer'))) .prompt(getPromptsForSchema(opts, schema, projectsConfigurations)) .then((values) => ({ ...opts, ...values })) .catch((e) => { console.error(e); process.exit(0); }); } function findSchemaForProperty(propName, schema) { 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 isTTY() { 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); }