From 2324bef0be9f59b2c242341950ceb25848898d9c Mon Sep 17 00:00:00 2001 From: Jon McClure Date: Sun, 5 Jan 2020 16:59:18 -0500 Subject: [PATCH] ready for release --- LICENSE.md | 22 + README.md | 168 +++++- async.js | 21 - badge.svg | 850 ++++++++++++++++++++++++++++++ cli.js | 0 cli/index.js | 59 +++ cli/utils/ensureFile.js | 10 + cli/utils/readJSON.js | 3 + cli/utils/writeJSON.js | 3 + index.js | 27 - lib/evaluateError.js | 14 +- lib/handlers/invalidBoolean.js | 25 +- lib/handlers/invalidNull.js | 2 +- lib/handlers/invalidNumber.js | 27 +- lib/handlers/invalidString.js | 27 +- lib/handlers/missingBoolean.js | 29 +- lib/handlers/missingItem.js | 17 +- lib/handlers/missingNumber.js | 29 +- lib/handlers/missingProperty.js | 8 +- lib/handlers/missingString.js | 29 +- lib/index.js | 40 ++ lib/utils/formatMessage.js | 7 + lib/utils/getArrayItemModifier.js | 17 - lib/utils/humanizeValueName.js | 17 + package.json | 14 +- test/invalidBoolean.js | 17 +- test/invalidNumbers.js | 49 +- test/invalidString.js | 23 +- test/missingArrayItem.js | 79 ++- test/missingProperties.js | 24 +- test/schemas/customPrompt.json | 23 + test/schemas/simple.json | 36 ++ yarn.lock | 91 +++- 33 files changed, 1591 insertions(+), 216 deletions(-) create mode 100644 LICENSE.md delete mode 100644 async.js create mode 100644 badge.svg delete mode 100644 cli.js create mode 100644 cli/index.js create mode 100644 cli/utils/ensureFile.js create mode 100644 cli/utils/readJSON.js create mode 100644 cli/utils/writeJSON.js delete mode 100644 index.js create mode 100644 lib/index.js create mode 100644 lib/utils/formatMessage.js delete mode 100644 lib/utils/getArrayItemModifier.js create mode 100644 lib/utils/humanizeValueName.js create mode 100644 test/schemas/customPrompt.json create mode 100644 test/schemas/simple.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..822823d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +Copyright (c) 2020 Reuters + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index f2079e1..d70ba8e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,172 @@ -# json-schema-prompts +![](badge.svg) +# ask-json + +Easy interactive prompts to create and validate data using [JSON schema](https://json-schema.org/). + +### Why this? + +We use JSON files to hold important configuration like the metadata for our published pages. But filling out those files is often tedious and prone to error. + +Using [JSON schema](https://json-schema.org/) helps us ensure config files contain the correct information. ask-json gives us a way to make filling out and validating those files easy. It hooks into our publishing process and creates an interactive step to validate configuration before it's used. + +### What's it do? + +ask-json lets you turn any JSON schema into an interactive CLI prompt. Just create a schema with some validation rules. ask-json will ask for any required information using friendly, data-type specific prompts using [prompts.js](https://www.npmjs.com/package/prompts). + +It's best used to check an existing JSON file, correct invalid and missing information and write that data back to the file. + +### Install + +``` +$ npm install ask-json +``` + +or + +``` +$ yarn add ask-json +``` + +### Prereqs: JSON schema + +ask-json is driven by JSON schema validation. Check out the [official docs](https://json-schema.org/understanding-json-schema/) to learn more about the spec. + +Internally, ask-json uses [ajv](https://ajv.js.org/) to validate data. + +### Use + +##### CLI + +Ask for data based on a JSON schema file. + +``` +$ askjson jsonSchema.json +``` + +Validate some data in a file. Answers to invalid or missing data will be rewritten to the file. + +``` +$ askjson jsonSchema.json -f output.json +``` + +##### Module + +```javascript +import askJSON from 'ask-json'; + +const jsonSchema = { + type: 'object', + properties: { + title: { + type: 'string', + }, + author: { + type: 'object', + properties: { + name: { + type: 'string', + }, + email: { + type: 'string', + format: 'email', + }, + }, + required: ['name', 'email'], + }, + }, + required: ['title', 'author'], +}; + +const testData = { + author: { name: 'Jon McClure' }, +}; + +// Will ask user for title and author.email data! +const data = await askJSON(jsonSchema, testData); +``` + +### Supported validation + +As of the current version, ask-json handles simple schema validation rules and not _all_ [keywords](https://ajv.js.org/#validation-keywords) are supported. + +These are validation keywords currently handled by type: +- `number` - maximum, minimum, exclusiveMaximum, exclusiveMinimum, multipleOf +- `string` - maxLength, minLength, pattern, format +- `array` - minItems, items +- `object` - required + +Arrays in ask-json can **only contain items of one type.** For example, ask-json does not support this schema: + + ```json + { + "type": "array", + "items": [{ + "type": "number" + }, { + "type": "string" + }] + } + ``` + + +Because ask-json uses validation errors to trigger questions, any fields you want to prompt for when missing **must be marked required.** For example, in the following schema, ask-json will never ask for the `email` property _unless it contains invalid data_. + + ```json + { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "required": [ + "name" + ] + } + ``` + + +### Customizing questions + +You can customize the questions used to ask users for missing or invalid data by adding a `prompt` property to your schema with [prompts.js](https://www.npmjs.com/package/prompts) question options. + +There are some conditions: You won't have access to the `name` property on your questions. Also, all functions will **not** have access to any previous answers -- e.g., `(prev, values) => { ... }`. + +Lastly, the `message` property does not follow the usual signature in prompts.js. Instead, you can supply a static string or a function which receives two params: the object dot-path of the variable being set and a message from ajv if the current value is invalid. + +Here's an example of some custom prompts: ```javascript +const schema = { + type: 'object', + email: { + type: 'string', + format: 'email', + prompt: { + message: (variablePath, invalidMessage = null) => { + if(!invalidMessage) return 'What\'s your email?' + return `What\'s your email? (${invalidMessage})` + } + } + } + color: { + type: 'color', + prompt: { + type: 'select', + choices: [ + { title: 'Red', value: '#ff0000' }, + { title: 'Green', value: '#00ff00' }, + { title: 'Blue', value: '#0000ff' }, + ], + } + } +} +``` + +### Testing ``` +$ yarn test +``` diff --git a/async.js b/async.js deleted file mode 100644 index 1360dd7..0000000 --- a/async.js +++ /dev/null @@ -1,21 +0,0 @@ -const prompts = require('prompts'); - -const asyncFunc = async() => { - const response = await prompts({ - type: 'number', - name: 'value', - message: 'How old are you?', - validate: value => value < 18 ? 'Nightclub is 18+ only' : true, - }); - - console.log('Answered!'); - - console.log(response); // => { value: 24 } -}; - -const runner = async() => { - await asyncFunc(); - console.log('WAITS'); -}; - -runner(); diff --git a/badge.svg b/badge.svg new file mode 100644 index 0000000..ae6cb5b --- /dev/null +++ b/badge.svg @@ -0,0 +1,850 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cli.js b/cli.js deleted file mode 100644 index e69de29..0000000 diff --git a/cli/index.js b/cli/index.js new file mode 100644 index 0000000..ecd0eec --- /dev/null +++ b/cli/index.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const ensureFile = require('./utils/ensureFile'); +const readJSON = require('./utils/readJSON'); +const writeJSON = require('./utils/writeJSON'); +const askJSON = require('../lib'); +const argv = require('yargs') + .command('$0 ', + 'Ask users for data based on JSON schema', (yargs) => { + yargs.positional('schema', { + describe: 'Path to a JSON schema file.', + type: 'string', + coerce: (filePath) => !filePath ? false : path.resolve(process.cwd(), filePath), + }) + .options({ + file: { + alias: 'f', + describe: 'Path to a JSON file. Per the JSON schema, users will be asked to supply missing and correct invalid data. Their answers will be written back to the file.', + type: 'string', + coerce: (filePath) => !filePath ? false : path.resolve(process.cwd(), filePath), + }, + inject: { + alias: 'i', + describe: 'Path to a JSON file with test data that will be injected into prompts (for testing purposes).', + type: 'string', + coerce: (filePath) => !filePath ? false : path.resolve(process.cwd(), filePath), + }, + }) + .help() + .version(); + }) + .argv; + +if (!fs.existsSync(argv.schema)) { + console.error(`\n⛔ Unable to read JSON schema file: ${chalk.green(argv.schema)}\n`); + process.exit(1); +} + +const SCHEMA = readJSON(argv.schema); +const INJECT_ANSWERS = argv.inject && fs.existsSync(argv.inject) ? + readJSON(argv.inject) : {}; +let DATA; + +const run = async() => { + if (!argv.file) { + DATA = await askJSON(SCHEMA, {}, INJECT_ANSWERS); + process.stdout.write(JSON.stringify(DATA, null, 2)); + process.exit(0); + } else { + ensureFile(argv.file); + const FILE = readJSON(argv.file); + DATA = await askJSON(SCHEMA, FILE, INJECT_ANSWERS); + writeJSON(argv.file, DATA); + } +}; + +run(); diff --git a/cli/utils/ensureFile.js b/cli/utils/ensureFile.js new file mode 100644 index 0000000..d17fb13 --- /dev/null +++ b/cli/utils/ensureFile.js @@ -0,0 +1,10 @@ +const path = require('path'); +const fs = require('fs'); + +module.exports = (filePath) => { + const dirname = path.dirname(filePath); + if (!fs.existsSync(filePath)) { + fs.mkdirSync(dirname, { recursive: true }); + fs.writeFileSync(filePath, '{}'); + }; +}; diff --git a/cli/utils/readJSON.js b/cli/utils/readJSON.js new file mode 100644 index 0000000..e554925 --- /dev/null +++ b/cli/utils/readJSON.js @@ -0,0 +1,3 @@ +const fs = require('fs'); + +module.exports = filePath => JSON.parse(fs.readFileSync(filePath)); diff --git a/cli/utils/writeJSON.js b/cli/utils/writeJSON.js new file mode 100644 index 0000000..4c88190 --- /dev/null +++ b/cli/utils/writeJSON.js @@ -0,0 +1,3 @@ +const fs = require('fs'); + +module.exports = (filePath, data) => fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); diff --git a/index.js b/index.js deleted file mode 100644 index d6eed0a..0000000 --- a/index.js +++ /dev/null @@ -1,27 +0,0 @@ -const Ajv = require('ajv'); -const evaluateError = require('./lib/evaluateError'); - -const ajv = new Ajv({ allErrors: true, verbose: true }); - -const handleErrors = async({ errors }, data, testValues) => { - // Execute each handler in series - for (const error of errors) { - await evaluateError(error, data, testValues); - } -}; - -const promptSchema = async(schema, data, testValues = {}, testOpts = { returnError: false, returnAfter: 0 }, maxRecursion = 20) => { - let valid = false; - let i = 0; - while (!valid && i < maxRecursion) { - valid = ajv.validate(schema, data); - if (!valid) { - await handleErrors(ajv, data, testValues); - if (testOpts.returnError && testOpts.returnAfter === i) return ajv.errors; - } - i += 1; - } - return data; -}; - -module.exports = promptSchema; diff --git a/lib/evaluateError.js b/lib/evaluateError.js index 137b374..a36a915 100644 --- a/lib/evaluateError.js +++ b/lib/evaluateError.js @@ -5,34 +5,34 @@ const handleInvalidBoolean = require('./handlers/invalidBoolean'); const handleMissingItem = require('./handlers/missingItem'); const handleInvalidNull = require('./handlers/invalidNull'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const { params, parentSchema, keyword } = error; // Handles missing number, integer, string, boolean, array, object or null if (params.missingProperty) { - return handleMissingProperty(error, data, testValues); + return handleMissingProperty(error, data, injectAnswers); } // Handles invalid number or integer if ( parentSchema.type === 'number' || parentSchema.type === 'integer' ) { - return handleInvalidNumber(error, data, testValues); + return handleInvalidNumber(error, data, injectAnswers); } // Handles invalid string if (parentSchema.type === 'string') { - return handleInvalidString(error, data, testValues); + return handleInvalidString(error, data, injectAnswers); } // Handles invalid boolean if (parentSchema.type === 'boolean') { - return handleInvalidBoolean(error, data, testValues); + return handleInvalidBoolean(error, data, injectAnswers); } // Handles invalid null if (parentSchema.type === 'null') { - return handleInvalidNull(error, data, testValues); + return handleInvalidNull(error, data, injectAnswers); } // Handles missing items if (keyword === 'minItems') { - return handleMissingItem(error, data, testValues); + return handleMissingItem(error, data, injectAnswers); } }; diff --git a/lib/handlers/invalidBoolean.js b/lib/handlers/invalidBoolean.js index 300ac32..c008832 100644 --- a/lib/handlers/invalidBoolean.js +++ b/lib/handlers/invalidBoolean.js @@ -1,23 +1,30 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); +const formatMessage = require('../utils/formatMessage'); +const humanize = require('../utils/humanizeValueName'); const set = require('lodash/set'); -const chalk = require('chalk'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const prompts = require('prompts'); const { dataPath, parentSchema } = error; const updatePath = prepareObjectPath(dataPath); - if (updatePath in testValues) { - prompts.inject([testValues[updatePath]]); + if (updatePath in injectAnswers) { + prompts.inject([injectAnswers[updatePath]]); } + const promptOpts = parentSchema.prompt || {}; + const prompt = Object.assign( + { type: 'confirm' }, + promptOpts, { - type: 'confirm', - message: `Should ${chalk.green(updatePath)} be true?\n`, - }, - parentSchema.prompt || {}, - { name: updatePath } + name: updatePath, + message: formatMessage( + updatePath, + promptOpts, + `Should ${humanize(updatePath)} be true?\n` + ), + } ); const answers = await prompts([prompt]); diff --git a/lib/handlers/invalidNull.js b/lib/handlers/invalidNull.js index 4075080..417a4bf 100644 --- a/lib/handlers/invalidNull.js +++ b/lib/handlers/invalidNull.js @@ -1,7 +1,7 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); const set = require('lodash/set'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const { dataPath } = error; const updatePath = prepareObjectPath(dataPath); diff --git a/lib/handlers/invalidNumber.js b/lib/handlers/invalidNumber.js index 481d9b5..eaeb4d0 100644 --- a/lib/handlers/invalidNumber.js +++ b/lib/handlers/invalidNumber.js @@ -1,23 +1,32 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); +const formatMessage = require('../utils/formatMessage'); +const humanize = require('../utils/humanizeValueName'); const set = require('lodash/set'); const chalk = require('chalk'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const prompts = require('prompts'); - const { dataPath, message, parentSchema } = error; + const { dataPath, message: validationMessage, parentSchema } = error; const updatePath = prepareObjectPath(dataPath); - if (updatePath in testValues) { - prompts.inject([testValues[updatePath]]); + if (updatePath in injectAnswers) { + prompts.inject([injectAnswers[updatePath]]); } + const promptOpts = parentSchema.prompt || {}; + const prompt = Object.assign( + { type: 'number' }, + promptOpts, { - type: 'number', - message: `What should the value of ${chalk.green(updatePath)} be?\n(${message})`, - }, - parentSchema.prompt || {}, - { name: updatePath } + name: updatePath, + message: formatMessage( + updatePath, + promptOpts, + `What should the value of ${humanize(updatePath)} be?\n${chalk.red(validationMessage)})`, + validationMessage + ), + } ); const answers = await prompts([prompt]); diff --git a/lib/handlers/invalidString.js b/lib/handlers/invalidString.js index 3a475ef..86aa504 100644 --- a/lib/handlers/invalidString.js +++ b/lib/handlers/invalidString.js @@ -1,23 +1,32 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); +const formatMessage = require('../utils/formatMessage'); +const humanize = require('../utils/humanizeValueName'); const set = require('lodash/set'); const chalk = require('chalk'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const prompts = require('prompts'); - const { dataPath, message, parentSchema } = error; + const { dataPath, message: validationMessage, parentSchema } = error; const updatePath = prepareObjectPath(dataPath); - if (updatePath in testValues) { - prompts.inject([testValues[updatePath]]); + if (updatePath in injectAnswers) { + prompts.inject([injectAnswers[updatePath]]); } + const promptOpts = parentSchema.prompt || {}; + const prompt = Object.assign( + { type: 'text' }, + promptOpts, { - type: 'text', - message: `What should the value of ${chalk.green(updatePath)} be?\n(${message})`, - }, - parentSchema.prompt || {}, - { name: updatePath } + name: updatePath, + message: formatMessage( + updatePath, + promptOpts, + `What should the value of ${humanize(updatePath)} be?\n${chalk.red(validationMessage)})`, + validationMessage + ), + } ); const answers = await prompts([prompt]); diff --git a/lib/handlers/missingBoolean.js b/lib/handlers/missingBoolean.js index 065dac0..870c4cc 100644 --- a/lib/handlers/missingBoolean.js +++ b/lib/handlers/missingBoolean.js @@ -1,27 +1,30 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); -const getArrayItemModifier = require('../utils/getArrayItemModifier'); +const formatMessage = require('../utils/formatMessage'); +const humanize = require('../utils/humanizeValueName'); const set = require('lodash/set'); -const chalk = require('chalk'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const prompts = require('prompts'); const { params, schema, dataPath } = error; const updatePath = prepareObjectPath(dataPath, params.missingProperty); - if (updatePath in testValues) { - prompts.inject([testValues[updatePath]]); + if (updatePath in injectAnswers) { + prompts.inject([injectAnswers[updatePath]]); } + const promptOpts = schema[params.missingProperty].prompt || {}; + const prompt = Object.assign( + { type: 'confirm' }, + promptOpts, { - type: 'confirm', - message: - `Should the value of \ -${chalk.green(params.missingProperty)} \ -be true${getArrayItemModifier(updatePath)}?\n`, - }, - schema.prompt || {}, - { name: updatePath } + name: updatePath, + message: formatMessage( + updatePath, + promptOpts, + `Should the value of ${humanize(updatePath)} be true?\n` + ), + } ); const answers = await prompts([prompt]); diff --git a/lib/handlers/missingItem.js b/lib/handlers/missingItem.js index 63406c4..39c8ed9 100644 --- a/lib/handlers/missingItem.js +++ b/lib/handlers/missingItem.js @@ -1,16 +1,23 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); -const objectPath = require('object-path'); +const set = require('lodash/set'); +const get = require('lodash/get'); -module.exports = async(error, data, testValues) => { +const pushToPath = (data, updatePath, item = null) => { + const arr = get(data, updatePath); + arr.push(item); + set(data, updatePath, arr); +}; + +module.exports = async(error, data, injectAnswers) => { return new Promise((resolve, reject) => { const { parentSchema, dataPath } = error; const updatePath = prepareObjectPath(dataPath); if (parentSchema.items.type === 'object') { - objectPath.push(data, updatePath, {}); + pushToPath(data, updatePath, {}); } if (parentSchema.items.type === 'array') { - objectPath.push(data, updatePath, []); + pushToPath(data, updatePath, []); } if ( parentSchema.items.type === 'number' || @@ -19,7 +26,7 @@ module.exports = async(error, data, testValues) => { parentSchema.items.type === 'boolean' || parentSchema.items.type === 'null' ) { - objectPath.push(data, updatePath, null); + pushToPath(data, updatePath, null); } resolve(); }); diff --git a/lib/handlers/missingNumber.js b/lib/handlers/missingNumber.js index 090aaa4..1512fed 100644 --- a/lib/handlers/missingNumber.js +++ b/lib/handlers/missingNumber.js @@ -1,27 +1,30 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); -const getArrayItemModifier = require('../utils/getArrayItemModifier'); +const formatMessage = require('../utils/formatMessage'); +const humanize = require('../utils/humanizeValueName'); const set = require('lodash/set'); -const chalk = require('chalk'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const prompts = require('prompts'); const { params, schema, dataPath } = error; const updatePath = prepareObjectPath(dataPath, params.missingProperty); - if (updatePath in testValues) { - prompts.inject([testValues[updatePath]]); + if (updatePath in injectAnswers) { + prompts.inject([injectAnswers[updatePath]]); } + const promptOpts = schema[params.missingProperty].prompt || {}; + const prompt = Object.assign( + { type: 'number' }, + promptOpts, { - type: 'number', - message: - `What should the value of \ -${chalk.green(params.missingProperty)} \ -be${getArrayItemModifier(updatePath)}?\n`, - }, - schema.prompt || {}, - { name: updatePath } + name: updatePath, + message: formatMessage( + updatePath, + promptOpts, + `What should the value of ${humanize(updatePath)} be?\n` + ), + } ); const answers = await prompts([prompt]); diff --git a/lib/handlers/missingProperty.js b/lib/handlers/missingProperty.js index d558fa3..733f357 100644 --- a/lib/handlers/missingProperty.js +++ b/lib/handlers/missingProperty.js @@ -5,7 +5,7 @@ const handleMissingString = require('./missingString'); const handleMissingNumber = require('./missingNumber'); const handleMissingBoolean = require('./missingBoolean'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const { params, schema } = error; switch (schema[params.missingProperty].type) { case 'object': @@ -18,14 +18,14 @@ module.exports = async(error, data, testValues) => { await handleMissingNull(error, data); break; case 'string': - await handleMissingString(error, data, testValues); + await handleMissingString(error, data, injectAnswers); break; case 'number': case 'integer': - await handleMissingNumber(error, data, testValues); + await handleMissingNumber(error, data, injectAnswers); break; case 'boolean': - await handleMissingBoolean(error, data, testValues); + await handleMissingBoolean(error, data, injectAnswers); break; default: break; diff --git a/lib/handlers/missingString.js b/lib/handlers/missingString.js index 4267642..622b300 100644 --- a/lib/handlers/missingString.js +++ b/lib/handlers/missingString.js @@ -1,27 +1,30 @@ const prepareObjectPath = require('../utils/prepareObjectPath'); -const getArrayItemModifier = require('../utils/getArrayItemModifier'); +const formatMessage = require('../utils/formatMessage'); +const humanize = require('../utils/humanizeValueName'); const set = require('lodash/set'); -const chalk = require('chalk'); -module.exports = async(error, data, testValues) => { +module.exports = async(error, data, injectAnswers) => { const prompts = require('prompts'); const { params, schema, dataPath } = error; const updatePath = prepareObjectPath(dataPath, params.missingProperty); - if (updatePath in testValues) { - prompts.inject([testValues[updatePath]]); + if (updatePath in injectAnswers) { + prompts.inject([injectAnswers[updatePath]]); } + const promptOpts = schema[params.missingProperty].prompt || {}; + const prompt = Object.assign( + { type: 'text' }, + promptOpts, { - type: 'text', - message: - `What should the value of \ -${chalk.green(params.missingProperty)} \ -be${getArrayItemModifier(updatePath)}?\n`, - }, - schema.prompt || {}, - { name: updatePath } + name: updatePath, + message: formatMessage( + updatePath, + promptOpts, + `What should the value of ${humanize(updatePath)} be?\n` + ), + } ); const answers = await prompts([prompt]); diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..e5c0b64 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,40 @@ +const Ajv = require('ajv'); +const evaluateError = require('./evaluateError'); + +// Need to capture process exit explicitly, as prompts +// handles this differently to escape each question... +process.stdin.on('keypress', (str, key) => { + if (key.ctrl && key.name === 'c') process.exit(); +}); + +const ajv = new Ajv({ allErrors: true, verbose: true }); + +const handleErrors = async({ errors }, data, injectAnswers) => { + for (const error of errors) { + await evaluateError(error, data, injectAnswers); + } +}; + +const askJSON = async(schema, data, injectAnswers = {}, maxRecursion = 100) => { + let valid = false; + let validate; + let i = 0; + + try { + validate = ajv.compile(schema); + } catch (e) { + console.error(`\n⛔ Invalid JSON schema!\n${e}\n`); + process.exit(1); + } + + while (!valid && i < maxRecursion) { + valid = validate(data); + if (!valid) { + await handleErrors(validate, data, injectAnswers); + } + i += 1; + } + return data; +}; + +module.exports = askJSON; diff --git a/lib/utils/formatMessage.js b/lib/utils/formatMessage.js new file mode 100644 index 0000000..c5765ec --- /dev/null +++ b/lib/utils/formatMessage.js @@ -0,0 +1,7 @@ +const isFunction = require('lodash/isFunction'); + +module.exports = (updatePath, promptOpts, defaultMessage, validationMessage = null) => { + if (!promptOpts.message) return defaultMessage; + if (isFunction(promptOpts.message)) return promptOpts.message(updatePath, validationMessage); + return promptOpts.message; +}; diff --git a/lib/utils/getArrayItemModifier.js b/lib/utils/getArrayItemModifier.js deleted file mode 100644 index e4201db..0000000 --- a/lib/utils/getArrayItemModifier.js +++ /dev/null @@ -1,17 +0,0 @@ -const ordinal = require('ordinal'); -const chalk = require('chalk'); - -module.exports = (updatePath) => { - // Match array paths like: authors[0].email - const arrayPathMatches = updatePath.match(/[^.[\]]+\[\d+\]\.[^.[\]]+$/); - - if (arrayPathMatches) { - const arrayPath = arrayPathMatches[0]; - // Strip out field name: authors[0].email => authors - const arrayField = arrayPath.replace(arrayPath.match(/\[\d+\].+$/), ''); - // Strip and parse to ordinal item number: authors[0].email => 1st - const itemNumber = ordinal(parseInt(arrayPath.match(/\[(?\d+)\]/).groups.digit) + 1); - return ` for the ${chalk.yellow(itemNumber)} item in ${chalk.green(arrayField)}`; - } - return ''; -}; diff --git a/lib/utils/humanizeValueName.js b/lib/utils/humanizeValueName.js new file mode 100644 index 0000000..1e6c31d --- /dev/null +++ b/lib/utils/humanizeValueName.js @@ -0,0 +1,17 @@ +const chalk = require('chalk'); + +module.exports = (updatePath) => { + // Either the last property value or property value and array(s) index + // author.name => name + // author.emails[0] => emails[0] + // authors.emails[0][0] => emails[0][0] + const fieldName = /\[\d+\]$/.test(updatePath) ? + updatePath.match(/[^.]+$/)[0] : + updatePath.match(/[^.[\]]+$/)[0]; + + if (fieldName === updatePath) return `${chalk.underline.green(fieldName)}`; + + const location = updatePath.replace(fieldName, '').replace(/\.$/, ''); + + return `${chalk.underline.green(fieldName)} ${chalk.grey(`(${location})`)}`; +}; diff --git a/package.json b/package.json index 35298f0..e7a9699 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,25 @@ { - "name": "json-schema-prompts", + "name": "ask-json", + "description": "Easy interactive prompts to create and validate data using JSON schema", "version": "0.0.1", "author": "Jon McClure ", + "homepage": "https://github.com/reuters-graphics/ask-json", + "repository": "github:reuters-graphics/ask-json", "license": "MIT", "private": false, "main": "lib/index.js", "bin": { - "promptjson": "cli.js" + "askjson": "cli/index.js" }, "scripts": { - "test": "mocha" + "test": "rm -f test/test.log && mocha" }, "dependencies": { "ajv": "^6.10.2", "chalk": "^3.0.0", "lodash": "^4.17.15", - "object-path": "^0.11.4", - "ordinal": "^1.0.3", - "prompts": "^2.3.0" + "prompts": "^2.3.0", + "yargs": "^15.1.0" }, "devDependencies": { "@hobbes7878/eslint-config": "^0.0.2", diff --git a/test/invalidBoolean.js b/test/invalidBoolean.js index 6d34ebc..596b324 100644 --- a/test/invalidBoolean.js +++ b/test/invalidBoolean.js @@ -1,8 +1,8 @@ const expect = require('expect.js'); -const promptSchema = require('..'); +const askJSON = require('../lib'); describe('Test invalid boolean', function() { - it('Should prompt for an invalid boolean', async function() { + it('Should ask for an invalid boolean', async function() { const schema = { type: 'object', properties: { @@ -17,18 +17,27 @@ describe('Test invalid boolean', function() { }, }, }, + arr: { + type: 'array', + items: { + type: 'boolean', + }, + }, }, }; const testData = { bool: 'not a boolean', obj: { nestedBool: 'not a boolean' }, + arr: ['not a boolean'], }; - const testValues = { + const injectAnswers = { bool: true, 'obj.nestedBool': false, + 'arr[0]': true, }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.bool).to.be(true); expect(data.obj.nestedBool).to.be(false); + expect(data.arr[0]).to.be(true); }); }); diff --git a/test/invalidNumbers.js b/test/invalidNumbers.js index c4c2491..f4440a6 100644 --- a/test/invalidNumbers.js +++ b/test/invalidNumbers.js @@ -1,8 +1,47 @@ const expect = require('expect.js'); -const promptSchema = require('..'); +const askJSON = require('../lib'); describe('Test invalid number', function() { - it('Should prompt for a number failing validation keywords', async function() { + it('Should ask for an invalid number', async function() { + const schema = { + type: 'object', + properties: { + num: { + type: 'number', + }, + obj: { + type: 'object', + properties: { + nestedNum: { + type: 'number', + }, + }, + }, + arr: { + type: 'array', + items: { + type: 'number', + }, + }, + }, + }; + const testData = { + num: 'not a num', + obj: { nestedNum: 'not a num' }, + arr: [null], + }; + const injectAnswers = { + num: 1, + 'obj.nestedNum': 2, + 'arr[0]': 3, + }; + const data = await askJSON(schema, testData, injectAnswers); + expect(data.num).to.be(1); + expect(data.obj.nestedNum).to.be(2); + expect(data.arr[0]).to.be(3); + }); + + it('Should ask for a number failing validation keywords', async function() { const schema = { type: 'object', properties: { @@ -13,7 +52,7 @@ describe('Test invalid number', function() { max: { type: 'number', maximum: 5, - prompt: { + ask: { message: 'This should be a special number?', }, }, @@ -33,13 +72,13 @@ describe('Test invalid number', function() { }, }; const testData = { min: 4, max: 6, multiple: 10, obj: { nestedMin: 2 } }; - const testValues = { + const injectAnswers = { min: 6, max: 4, multiple: 9, 'obj.nestedMin': 20, }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.min).to.be(6); expect(data.max).to.be(4); expect(data.multiple).to.be(9); diff --git a/test/invalidString.js b/test/invalidString.js index 30d56e5..9954db3 100644 --- a/test/invalidString.js +++ b/test/invalidString.js @@ -1,8 +1,8 @@ const expect = require('expect.js'); -const promptSchema = require('..'); +const askJSON = require('../lib'); describe('Test invalid string', function() { - it('Should prompt for an invalid string', async function() { + it('Should ask for an invalid string', async function() { const schema = { type: 'object', properties: { @@ -17,22 +17,31 @@ describe('Test invalid string', function() { }, }, }, + arr: { + type: 'array', + items: { + type: 'string', + }, + }, }, }; const testData = { str: null, obj: { nestedStr: false }, + arr: [null], }; - const testValues = { + const injectAnswers = { str: 'asd', 'obj.nestedStr': 'dsa', + 'arr[0]': 'qwe', }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.str).to.be('asd'); expect(data.obj.nestedStr).to.be('dsa'); + expect(data.arr[0]).to.be('qwe'); }); - it('Should prompt for an invalid string format', async function() { + it('Should ask for a string failing validation format', async function() { const schema = { type: 'object', properties: { @@ -61,13 +70,13 @@ describe('Test invalid string', function() { uri: 'not a uri', }; const datetime = new Date(); - const testValues = { + const injectAnswers = { date: '2012-12-12', datetime: datetime.toISOString(), email: 'jon.r.mcclure@gmail.com', uri: 'http://google.com', }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.date).to.be('2012-12-12'); expect(data.datetime).to.be(datetime.toISOString()); expect(data.email).to.be('jon.r.mcclure@gmail.com'); diff --git a/test/missingArrayItem.js b/test/missingArrayItem.js index 5d99275..71da1f6 100644 --- a/test/missingArrayItem.js +++ b/test/missingArrayItem.js @@ -1,8 +1,8 @@ const expect = require('expect.js'); -const promptSchema = require('..'); +const askJSON = require('../lib'); describe('Test missing items in an array', function() { - it('Should prompt for missing objects in an array', async function() { + it('Should ask for missing objects in an array', async function() { const schema = { type: 'object', properties: { @@ -14,30 +14,67 @@ describe('Test missing items in an array', function() { name: { type: 'string', }, - email: { - type: 'string', - format: 'email', + contacts: { + type: 'array', + items: { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + }, + }, + required: ['email'], + }, + minItems: 1, }, }, - required: ['name', 'email'], + required: ['name', 'contacts'], }, minItems: 2, }, }, }; const testData = { collection: [] }; - const testValues = { + const injectAnswers = { 'collection[0].name': 'Jon', - 'collection[0].email': 'jon.r.mcclure@gmail.com', + 'collection[0].contacts[0].email': 'jon.r.mcclure@gmail.com', 'collection[1].name': 'Lisa', - 'collection[1].email': 'lisa@gmail.com', + 'collection[1].contacts[0].email': 'lisa@gmail.com', }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.collection[0].name).to.be('Jon'); - expect(data.collection[1].email).to.be('lisa@gmail.com'); + expect(data.collection[1].contacts[0].email).to.be('lisa@gmail.com'); + }); + + it('Should ask for missing arrays in an array', async function() { + const schema = { + type: 'object', + properties: { + collection: { + type: 'array', + items: { + type: 'array', + items: { + type: 'string', + }, + minItems: 2, + }, + minItems: 1, + }, + }, + }; + const testData = { collection: [] }; + const injectAnswers = { + 'collection[0][0]': 'Jon', + 'collection[0][1]': 'Lisa', + }; + const data = await askJSON(schema, testData, injectAnswers); + expect(data.collection[0][0]).to.be('Jon'); + expect(data.collection[0][1]).to.be('Lisa'); }); - it('Should prompt for missing strings in an array', async function() { + it('Should ask for missing strings in an array', async function() { const schema = { type: 'object', properties: { @@ -52,16 +89,16 @@ describe('Test missing items in an array', function() { }, }; const testData = { collection: [] }; - const testValues = { + const injectAnswers = { 'collection[0]': 'j@gmail.com', 'collection[1]': 'l@gmail.com', }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.collection[0]).to.be('j@gmail.com'); expect(data.collection[1]).to.be('l@gmail.com'); }); - it('Should prompt for missing numbers in an array', async function() { + it('Should ask for missing numbers in an array', async function() { const schema = { type: 'object', properties: { @@ -75,16 +112,16 @@ describe('Test missing items in an array', function() { }, }; const testData = { collection: [] }; - const testValues = { + const injectAnswers = { 'collection[0]': 1, 'collection[1]': 2, }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.collection[0]).to.be(1); expect(data.collection[1]).to.be(2); }); - it('Should prompt for missing booleans in an array', async function() { + it('Should ask for missing booleans in an array', async function() { const schema = { type: 'object', properties: { @@ -98,10 +135,10 @@ describe('Test missing items in an array', function() { }, }; const testData = { collection: [] }; - const testValues = { + const injectAnswers = { 'collection[0]': true, }; - const data = await promptSchema(schema, testData, testValues); + const data = await askJSON(schema, testData, injectAnswers); expect(data.collection[0]).to.be(true); }); @@ -119,7 +156,7 @@ describe('Test missing items in an array', function() { }, }; const testData = { collection: [] }; - const data = await promptSchema(schema, testData); + const data = await askJSON(schema, testData); expect(data.collection[0]).to.be(null); expect(data.collection[1]).to.be(null); }); diff --git a/test/missingProperties.js b/test/missingProperties.js index 69977fd..14b0599 100644 --- a/test/missingProperties.js +++ b/test/missingProperties.js @@ -1,5 +1,5 @@ const expect = require('expect.js'); -const promptSchema = require('..'); +const askJSON = require('../lib'); describe('Test missing properties', function() { it('Should attach a missing object', async function() { @@ -18,7 +18,7 @@ describe('Test missing properties', function() { }, required: ['missingObject'], }; - const data = await promptSchema(schema, {}); + const data = await askJSON(schema, {}); expect(data.missingObject).to.be.an('object'); expect(data.missingObject.missingNestedObject).to.be.an('object'); }); @@ -42,12 +42,12 @@ describe('Test missing properties', function() { }, required: ['missingArray', 'object'], }; - const data = await promptSchema(schema, {}); + const data = await askJSON(schema, {}); expect(data.missingArray).to.be.an('array'); expect(data.object.missingNestedArray).to.be.an('array'); }); - it('Should prompt for a missing string', async function() { + it('Should ask for a missing string', async function() { const schema = { type: 'object', properties: { @@ -67,16 +67,16 @@ describe('Test missing properties', function() { required: ['missingString', 'object'], }; - const testValues = { + const injectAnswers = { missingString: 'asd', 'object.missingNestedString': 'qwe', }; - const data = await promptSchema(schema, {}, testValues); + const data = await askJSON(schema, {}, injectAnswers); expect(data.missingString).to.be('asd'); expect(data.object.missingNestedString).to.be('qwe'); }); - it('Should prompt for a missing number', async function() { + it('Should ask for a missing number', async function() { const schema = { type: 'object', properties: { @@ -96,16 +96,16 @@ describe('Test missing properties', function() { required: ['missingNumber', 'object'], }; - const testValues = { + const injectAnswers = { missingNumber: 1, 'object.missingNestedNumber': 2, }; - const data = await promptSchema(schema, {}, testValues); + const data = await askJSON(schema, {}, injectAnswers); expect(data.missingNumber).to.be(1); expect(data.object.missingNestedNumber).to.be(2); }); - it('Should prompt for a missing boolean', async function() { + it('Should ask for a missing boolean', async function() { const schema = { type: 'object', properties: { @@ -125,11 +125,11 @@ describe('Test missing properties', function() { required: ['missingBool', 'object'], }; - const testValues = { + const injectAnswers = { missingBool: true, 'object.missingNestedBool': false, }; - const data = await promptSchema(schema, {}, testValues); + const data = await askJSON(schema, {}, injectAnswers); expect(data.missingBool).to.be(true); expect(data.object.missingNestedBool).to.be(false); }); diff --git a/test/schemas/customPrompt.json b/test/schemas/customPrompt.json new file mode 100644 index 0000000..8e1ed88 --- /dev/null +++ b/test/schemas/customPrompt.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "properties": { + "color": { + "type": "string", + "prompt": { + "type": "select", + "message": "What's your favorite color?", + "choices": [ + { + "title": "Red", + "value": "#ff0000" + }, + { + "title": "Blue", + "value": "#00ff00" + } + ] + } + } + }, + "required": ["color"] +} diff --git a/test/schemas/simple.json b/test/schemas/simple.json new file mode 100644 index 0000000..1dd4a01 --- /dev/null +++ b/test/schemas/simple.json @@ -0,0 +1,36 @@ +{ + "type": "object", + "properties": { + "authors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": ["email"] + }, + "minItems": 1 + }, + "active": { + "type": "boolean" + } + }, + "required": ["name", "contacts", "active"] + }, + "minItems": 2 + } + }, + "required": ["authors"] +} diff --git a/yarn.lock b/yarn.lock index 1edfc11..34e5358 100644 --- a/yarn.lock +++ b/yarn.lock @@ -163,7 +163,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -283,6 +283,15 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -713,6 +722,14 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -1052,6 +1069,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -1187,11 +1211,6 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-path@^0.11.4: - version "0.11.4" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" - integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= - object.assign@4.1.0, object.assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" @@ -1266,11 +1285,6 @@ optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" -ordinal@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ordinal/-/ordinal-1.0.3.tgz#1a3c7726a61728112f50944ad7c35c06ae3a0d4d" - integrity sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ== - os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -1290,6 +1304,13 @@ p-limit@^2.0.0: dependencies: p-try "^2.0.0" +p-limit@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -1304,6 +1325,13 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -1333,6 +1361,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -1586,7 +1619,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -1770,6 +1803,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -1795,6 +1837,14 @@ yargs-parser@13.1.1, yargs-parser@^13.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^16.1.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" + integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-unparser@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" @@ -1819,3 +1869,20 @@ yargs@13.3.0, yargs@^13.3.0: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^13.1.1" + +yargs@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219" + integrity sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^16.1.0"